Logger Architecture Solution Summary
The Problem
Section titled “The Problem”The monorepo had one logger with mandatory observability, causing cascading issues:
ANY SHARED PACKAGE ↓imports @repo/utils/logger ↓re-exports from @repo/observability/logger ↓pulls in ALL OTEL packages ↓Astro v5 bundles CommonJS code ↓"require is not defined" ❌Affected packages:
@repo/auth- Can’t be imported without OTEL@repo/db- Can’t be imported without OTEL@repo/utils- Can’t be imported without OTEL@repo/ai- Can’t be imported without OTEL@repo/crm- Can’t be imported without OTEL
Build failures:
- ❌ CRM app fails to build (imports @repo/crm which imports logger)
- ⚠️ Any new app with minimal dependencies would hit the same issue
The Solution
Section titled “The Solution”Decoupled logger into two layers:
Layer 1: Base Logger (@repo/utils/logger)
Section titled “Layer 1: Base Logger (@repo/utils/logger)”// Pure Pino - NO observability dependencies// ✅ Works in all contexts (Node, browser, Astro build)// ✅ Lazy-loaded to avoid prerendering issues// ✅ Used by ALL shared packages
import { logger } from '@repo/utils/logger';logger.info('message'); // Simple Pino structured loggingLayer 2: Enhanced Logger (@repo/observability/logger)
Section titled “Layer 2: Enhanced Logger (@repo/observability/logger)”// OTEL + Pino - Full observability// ✅ Optional - only imported by apps that need it// ✅ Adds trace context + metrics// ✅ Used by TypeScript/JavaScript apps: webapp
import { logger } from '@repo/observability/logger';logger.info('message'); // Includes trace contextArchitecture Diagram
Section titled “Architecture Diagram”BEFORE (Monolithic)
Section titled “BEFORE (Monolithic)”Shared Packages ↓@repo/utils/logger (was just a re-export) ↓@repo/observability/logger ↓OTEL Packages (forced on everyone)AFTER (Modular)
Section titled “AFTER (Modular)”Shared Packages (@repo/auth, @repo/db, etc.) ↓@repo/utils/logger (base Pino) ↓No OTEL dependencies! ✅
Apps with Observability (webapp - TypeScript/JavaScript only) ↓@repo/observability/logger (optional enhancement) ↓OTEL Packages (only when needed)
Apps without Observability (CRM, standalone tools) ↓@repo/utils/logger (simple, lightweight) ↓No unnecessary dependencies! ✅Key Design Decisions
Section titled “Key Design Decisions”1. Lazy Loading in Base Logger
Section titled “1. Lazy Loading in Base Logger”// Pino only loads when first logger method is calledlet _logger: LogLike | null = null;
const loggerProxy = { get info() { return (_logger ??= createLogger()).info.bind(_logger); },};Why? Prevents Pino from being imported during Astro prerendering.
2. Environment Detection
Section titled “2. Environment Detection”const isNode = typeof process !== 'undefined' && !!(process as any).versions?.node;
if (!isNode) { // Browser context - use console return createConsoleLogger();}// Node context - use PinoWhy? Logger works everywhere without conditional imports.
3. Single LogLike Interface
Section titled “3. Single LogLike Interface”export interface LogLike { debug(...args): void; info(...args): void; warn(...args): void; error(...args): void; child(bindings?): LogLike;}Why? Type-safe across both base and enhanced loggers.
4. Optional OTEL Enhancement
Section titled “4. Optional OTEL Enhancement”// Apps can initialize OTEL separatelyif (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { const sdk = initializeOpenTelemetry();}// Base logger logs go through OTEL instrumentationWhy? Decouples logger initialization from OTEL initialization.
Usage Examples
Section titled “Usage Examples”Shared Package (Use Base Logger)
Section titled “Shared Package (Use Base Logger)”import { logger } from '@repo/utils/logger'; // ✅ Base logger
export async function authenticateUser(token: string) { logger.info('Authenticating user'); // ...}App WITH Observability (Use Enhanced Logger)
Section titled “App WITH Observability (Use Enhanced Logger)”import { logger } from '@repo/observability/logger'; // ✅ Enhanced
export const GET: APIRoute = async () => { logger.info('Request received'); // Includes trace context // ...};App WITHOUT Observability (Use Base Logger)
Section titled “App WITHOUT Observability (Use Base Logger)”import { logger } from '@repo/utils/logger'; // ✅ Base logger
export default logger;What This Fixes
Section titled “What This Fixes”| Issue | Status |
|---|---|
| CRM “require is not defined” error | ✅ FIXED |
| Forced OTEL dependencies in all packages | ✅ FIXED |
| Shared packages can’t use logger without OTEL | ✅ FIXED |
| Circular dependency risks | ✅ REDUCED |
| Build performance with OTEL overhead | ✅ IMPROVED |
| Type safety across loggers | ✅ IMPROVED |
Migration Path
Section titled “Migration Path”Phase 1: ✅ COMPLETE
- Create base logger in @repo/utils
- Document architecture
- Export LogLike interface
Phase 2: TODO
- Update shared packages to use @repo/utils/logger
- @repo/auth
- @repo/db
- @repo/ai
- @repo/crm
Phase 3: TODO
- Verify all builds work
- Test CRM build (no OTEL errors)
- Test webapp build (with OTEL)
- Run test suites
Files Created/Modified
Section titled “Files Created/Modified”Created
Section titled “Created”packages/utils/LOGGER_ARCHITECTURE.md- Detailed architecture guideLOGGER_MIGRATION_GUIDE.md- Migration instructionsLOGGER_SOLUTION_SUMMARY.md- This file
Modified
Section titled “Modified”packages/utils/src/logger.ts- Now contains base Pino implementationpackages/observability/src/logger.ts- Now enhancement layer (unchanged)
Benefits Summary
Section titled “Benefits Summary”✅ Observability is Optional
- Apps choose their logging level
- No forced dependencies
✅ Builds Just Work
- No more CommonJS bundling issues
- Astro v5 ESM works everywhere
✅ Clean Architecture
- Shared packages stay focused
- Clear separation of concerns
- Single responsibility per layer
✅ Future Proof
- Easy to add new loggers (e.g., structured JSON logs)
- Easy to add custom transports
- Easy to add log sampling/filtering
Next Steps
Section titled “Next Steps”- Review architecture - See LOGGER_ARCHITECTURE.md
- Update shared packages - Import from @repo/utils/logger
- Verify builds - Run build tests for all apps
- Document usage - Update READMEs for each app
This solution eliminates the “build time vs observability” tradeoff by making observability optional rather than mandatory.