Skip to content

Logger Architecture Solution Summary

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

Decoupled logger into two layers:

// 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 logging

Layer 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 context
Shared Packages
@repo/utils/logger (was just a re-export)
@repo/observability/logger
OTEL Packages (forced on everyone)
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! ✅
// Pino only loads when first logger method is called
let _logger: LogLike | null = null;
const loggerProxy = {
get info() {
return (_logger ??= createLogger()).info.bind(_logger);
},
};

Why? Prevents Pino from being imported during Astro prerendering.

const isNode = typeof process !== 'undefined' && !!(process as any).versions?.node;
if (!isNode) {
// Browser context - use console
return createConsoleLogger();
}
// Node context - use Pino

Why? Logger works everywhere without conditional imports.

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.

// Apps can initialize OTEL separately
if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
const sdk = initializeOpenTelemetry();
}
// Base logger logs go through OTEL instrumentation

Why? Decouples logger initialization from OTEL initialization.

packages/auth/src/server.ts
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)”
apps/webapp/src/...
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)”
apps/crm/src/utils/logger.ts
import { logger } from '@repo/utils/logger'; // ✅ Base logger
export default logger;
IssueStatus
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

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
  • packages/utils/LOGGER_ARCHITECTURE.md - Detailed architecture guide
  • LOGGER_MIGRATION_GUIDE.md - Migration instructions
  • LOGGER_SOLUTION_SUMMARY.md - This file
  • packages/utils/src/logger.ts - Now contains base Pino implementation
  • packages/observability/src/logger.ts - Now enhancement layer (unchanged)

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
  1. Review architecture - See LOGGER_ARCHITECTURE.md
  2. Update shared packages - Import from @repo/utils/logger
  3. Verify builds - Run build tests for all apps
  4. Document usage - Update READMEs for each app

This solution eliminates the “build time vs observability” tradeoff by making observability optional rather than mandatory.