Skip to content

Logger Usage Quick Reference

Category Import From Reason
Shared Packages @repo/utils/logger Never pull in observability
Apps WITH Observability @repo/observability/logger Full OTEL support
Apps WITHOUT Observability @repo/utils/logger Lightweight, no OTEL
  • βœ… packages/auth/
  • βœ… packages/db/
  • βœ… packages/utils/
  • βœ… packages/ai/
  • βœ… packages/pricing/
  • βœ… packages/integrations/
  • βœ… packages/ui/ (if logging)
  • βœ… packages/observability/ (for any app-independent code)
packages/auth/src/middleware.ts
import { logger } from '@repo/utils/logger';
export async function validateRequest(req: Request) {
logger.info('Validating request');
// ...
}
  • Keeps shared packages independent
  • Avoids circular dependencies
  • Prevents forced observability bundling
  • Lightweight apps (internal, Cloudflare Workers) can consume packages without OTEL

  • apps/webapp/
  • apps/ts-temporal-worker/
import { logger } from '@repo/observability/logger';
export const POST: APIRoute = async ({ request }) => {
logger.info('Processing request'); // βœ… Includes trace context
// ...
};

Note: Python apps (temporal-worker, dagster) use their own Python-based logging and OpenTelemetry instrumentation (apps/temporal-worker/observability.py, apps/dagster/erp_pipeline/observability.py), not the TypeScript @repo/observability package.

  • Gets full OTEL trace context automatically
  • Supports @opentelemetry/api-logs (OTEL_LOGS_EXPORTER=otlp) and @logtail/pino alongside stdout
  • Debugging with trace correlation

  • apps/internal/ (Cloud Run with _HAS_OTEL=false)
  • Cloudflare Workers (apps/email-worker/, apps/ops-mcp/, apps/content-mcp/, apps/partner-data-api/, apps/tidycal-hubspot-sync/)
  • apps/docs/ (static site)
import { logger } from '@repo/utils/logger';
logger.info('App started');
logger.error('Something failed', error);
  • No OTEL overhead
  • Faster builds and smaller worker bundles
  • Simpler dependencies
  • Still get structured Pino logging on Node or formatted console output on Workers/browser

πŸ”€ Argument Normalization (Both Call Styles Supported)

Section titled β€œπŸ”€ Argument Normalization (Both Call Styles Supported)”

Standard Pino expects (mergingObject, message) and silently drops objects passed after the message string. Both @repo/utils/logger (normalizeLogArgs) and @repo/observability/logger (buildOtelLogRecord / toPinoArgs) normalize arguments automatically so both call styles preserve all structured fields:

// βœ… Message-first (with context object and/or Error in any trailing position)
logger.info('Order extracted', { orderId, itemCount: 5 });
logger.error('ERP submission failed', err, { orderId, connectionId });
logger.error('ERP submission failed', { orderId, connectionId }, err);
// βœ… Pino-style object-first
logger.info({ orderId, itemCount: 5 }, 'Order extracted');
  • Plain objects are merged into top-level structured log attributes.
  • Error instances are automatically extracted (err in @repo/utils/logger; error.message and error.stack in @repo/observability/logger).

Terminal window
# Find all logger imports
grep -r "from '@repo/observability/logger'" packages/*/src
grep -r "from '@repo/observability/logger'" apps/*/src
// ❌ BEFORE
import { logger } from '@repo/observability/logger';
// βœ… AFTER
import { logger } from '@repo/utils/logger';
Terminal window
pnpm --filter webapp build
pnpm --filter internal build

Note: Python services (temporal-worker, dagster, packages/pdf-shared) use pytest and are not tested via pnpm build commands.


packages/
β”œβ”€β”€ auth/ ← βœ… import from @repo/utils/logger
β”œβ”€β”€ db/ ← βœ… import from @repo/utils/logger
β”œβ”€β”€ ui/ ← βœ… import from @repo/utils/logger
β”œβ”€β”€ utils/ ← βœ… import from @repo/utils/logger
β”œβ”€β”€ ai/ ← βœ… import from @repo/utils/logger
β”œβ”€β”€ pricing/ ← βœ… import from @repo/utils/logger
β”œβ”€β”€ integrations/ ← βœ… import from @repo/utils/logger
└── observability/ ← βœ… Enhanced logger layer (OTEL)
apps/
β”œβ”€β”€ webapp/ ← πŸš€ import from @repo/observability/logger (full OTEL)
β”œβ”€β”€ ts-temporal-worker/ ← πŸš€ import from @repo/observability/logger (full OTEL)
β”œβ”€β”€ internal/ ← πŸ› οΈ import from @repo/utils/logger (lightweight)
β”œβ”€β”€ email-worker/ ← πŸ› οΈ import from @repo/utils/logger (Cloudflare Worker)
β”œβ”€β”€ ops-mcp/ ← πŸ› οΈ import from @repo/utils/logger (Cloudflare Worker)
└── docs/ ← πŸ› οΈ import from @repo/utils/logger (static site)

// First import
import { logger } from '@repo/utils/logger';
// Pino not loaded yet (lazy)
logger.info('Message'); // ← Pino loads here
// Next import - reuses same instance
import { logger as logger2 } from '@repo/utils/logger';
logger2.info('Message'); // ← Uses cached instance
import { logger } from '@repo/utils/logger';
// Node.js server
logger.info('message'); // β†’ Pino structured log to stdout
// Browser/Client
logger.info('message'); // β†’ console.log
// Astro build time
logger.info('message'); // β†’ Simple Pino (no transports)

❌ DON’T: Import observability logger in shared packages

Section titled β€œβŒ DON’T: Import observability logger in shared packages”
packages/auth/src/server.ts
import { logger } from '@repo/observability/logger'; // ❌ WRONG

Problem: Forces OTEL on all consumers

Solution: Import from utils instead

import { logger } from '@repo/utils/logger'; // βœ… CORRECT

❌ DON’T: Conditionally import based on observability

Section titled β€œβŒ DON’T: Conditionally import based on observability”
let logger;
if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
logger = await import('@repo/observability/logger');
} else {
logger = await import('@repo/utils/logger');
}

Problem: Confusing and unnecessary

Solution: Always use base logger in shared code; let the app layer handle OTEL.


// βœ… GOOD
import { logger } from '@repo/utils/logger';
export function myFunction() {
logger.info('message');
}
// βœ… GOOD - adds context to all child logs
const requestLogger = logger.child({ requestId, userId });
requestLogger.info('Processing');
requestLogger.error('Failed');
// βœ… GOOD
logger.info('User created', { userId, email, timestamp: Date.now() });
// ❌ BAD
logger.info(`User ${userId} created at ${new Date()}`);
logger.debug('Detailed trace info'); // βœ… Development
logger.info('Normal operation'); // βœ… Important events
logger.warn('Recoverable issue'); // βœ… Warnings
logger.error('Error details', error, { context }); // βœ… Failures


Key Takeaway: Use @repo/utils/logger everywhere by default. Only use @repo/observability/logger in TypeScript/JavaScript apps that explicitly run with OpenTelemetry (webapp, ts-temporal-worker).