Logging
Explicit logger imports for server (Pino) and client (console wrapper).
Complete guide for using the logging system across the monorepo.
Overview
The monorepo uses explicit subpath imports:
- Server-side (
@repo/utils/logger/server): Fastify API and Next.js server runtime — Pino for structured JSON logging - Client-side (
@repo/utils/logger/client): Next.js client components — console wrapper with environment-controlled levels
Key principles:
- Server:
import { logger } from '@repo/utils/logger/server' - Client:
import { logger } from '@repo/utils/logger/client' - Never use
console.*directly in app code (enforced by linting) - Structured logging with support for bindings (requestId, userId, etc.)
- Environment-based configuration (enabled/disabled, log levels)
- Automatic secret redaction on server-side
Quick Start
// Server (Fastify, Next API routes, Node scripts)
import { logger } from '@repo/utils/logger/server'
// Client ('use client' components, browser)
import { logger } from '@repo/utils/logger/client'
logger.info({ userId: '123' }, 'User logged in')
logger.error({ error }, 'Request failed')Usage
Server-side
Fastify routes: prefer request.log for automatic request context.
// apps/fastify/src/routes/users.ts
import type { FastifyInstance } from 'fastify'
export async function userRoutes(fastify: FastifyInstance) {
fastify.get<{ Params: { id: string } }>('/users/:id', async (request, reply) => {
const { id } = request.params
request.log.info({ userId: id }, 'Fetching user')
try {
const user = await getUserById(id)
request.log.info({ userId: id }, 'User fetched')
return reply.send(user)
} catch (error) {
request.log.error({ error, userId: id }, 'Failed to fetch user')
return reply.code(500).send({ error: 'Internal server error' })
}
})
}Next.js route handlers & server actions: use logger (optionally child() for correlation).
// app/api/users/route.ts
import { logger } from '@repo/utils/logger/server'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const requestLogger = logger.child({
requestId: request.headers.get('x-request-id') || crypto.randomUUID(),
})
try {
requestLogger.info({}, 'Processing request')
const users = await getUsers()
requestLogger.debug({ count: users.length }, 'Users fetched')
return NextResponse.json(users)
} catch (error) {
requestLogger.error({ error }, 'Request failed')
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}Client-side
Client logs are disabled by default in production.
// components/user-profile.tsx
'use client'
import { logger } from '@repo/utils/logger/client'
import { useEffect } from 'react'
export function UserProfile({ userId }: { userId: string }) {
useEffect(() => {
logger.info({ component: 'UserProfile', userId }, 'Component mounted')
return () => logger.debug({ component: 'UserProfile', userId }, 'Component unmounted')
}, [userId])
const handleClick = () => {
logger.info({ component: 'UserProfile', userId }, 'User profile clicked')
}
return <div onClick={handleClick}>...</div>
}Environment Variables
Server-side (Fastify API, Next.js Server)
| Variable | Type | Default | Description |
|---|---|---|---|
LOG_ENABLED | boolean | true | Enable/disable logging |
LOG_LEVEL | debug|info|warn|error|silent | info | Minimum log level |
LOG_SERVICE | string | "app" | Service name for log entries |
Example .env:
LOG_ENABLED=true
LOG_LEVEL=info
LOG_SERVICE=apiClient-side (Next.js Client Components)
| Variable | Type | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_LOG_ENABLED | boolean | true (dev), false (prod) | Enable/disable logging |
NEXT_PUBLIC_LOG_LEVEL | debug|info|warn|error|silent | debug (dev), info (prod) | Minimum log level |
Example .env:
NEXT_PUBLIC_LOG_ENABLED=true
NEXT_PUBLIC_LOG_LEVEL=infoImportant: Client-side logs are disabled in production by default. This means log statements won't execute, improving performance and reducing bundle size.
CI and Test Environments
When running in CI (CI=true), or when NODE_ENV=test or VITEST is set (e.g. Vitest, Playwright E2E), the default log level is silent for both server and client. This reduces noise during tests. To enable logs for debugging, set LOG_LEVEL=debug (or info, warn, error) in .env.test or pass it when running tests.
Architecture
Explicit Subpath Imports
The logger uses explicit subpath imports — no conditional routing. Each entrypoint exports logger, Logger, and LogLevel:
@repo/utils/logger/server— Pino-based, for Node/Fastify/Next.js server@repo/utils/logger/client— console-based, for browser/client components
Consumers import the appropriate subpath based on their runtime environment.
Why No Runtime Detection?
Runtime detection (typeof window, process checks) has several problems:
- Bundle size: Both implementations get bundled, increasing size
- Tree-shaking fails: Bundlers can't eliminate unused code
- Edge runtime issues: Edge runtimes may not have expected globals
- Type safety: TypeScript can't properly narrow types
Explicit imports avoid these issues: each file imports the logger that matches its runtime.
Log Levels
Use appropriate log levels:
debug: Detailed information for debugging (development only)info: General informational messageswarn: Warning messages for potentially harmful situationserror: Error messages for failuressilent: Disable all logging
logger.debug({ data }, 'Detailed debug information')
logger.info({ userId }, 'User logged in')
logger.warn({ remaining: 10 }, 'Rate limit approaching')
logger.error({ error }, 'Database connection failed')Best Practices
1. Use Structured Fields - Always use structured fields instead of string interpolation:
// ✅ Good
logger.info({ userId, action: 'login' }, 'User logged in')
// ❌ Bad
logger.info(`User ${userId} logged in`)2. Request Correlation - Use child() to create request-scoped loggers:
const requestLogger = logger.child({ requestId, userId })
requestLogger.info('Processing request') // Includes requestId and userId3. Secret Redaction - Server-side logger automatically redacts: password, token, secret, key, apiKey, accessToken, refreshToken, authorization, cookie, session
4. Error Logging - Always include error objects:
try {
await processOrder(orderData)
} catch (error) {
logger.error({ error, context: { userId, orderId } }, 'Failed to process order')
throw error
}5. Integration with @repo/sentry - When using captureError in Fastify, pass request.log to use Fastify's native logger. The catalog and response are built in the app (e.g. from apps/fastify/src/lib/catalogs/):
import { captureError } from '@repo/sentry/node'
fastify.setErrorHandler((error, request, reply) => {
captureError({
error,
logger: request.log, // Uses Fastify's logger with request context
label: `${request.method} ${request.url}`,
tags: { app: 'api' },
})
// App builds safe response from its own catalog (getError, mapHttpStatusToErrorCode)
const catalogError = getError(mapHttpStatusToErrorCode(error.statusCode)) ?? getError('UNEXPECTED_ERROR')
reply.status(error.statusCode ?? 500).send(catalogError)
})Troubleshooting
Logs not appearing:
- Check
LOG_ENABLED/NEXT_PUBLIC_LOG_ENABLEDenvironment variables - Verify log level isn't higher than message level
- Remember: client logs disabled in production by default
Console usage linting errors:
- Ensure importing from
@repo/utils/logger/serveror@repo/utils/logger/client(notconsole.*) - Only
packages/utils/logger/client.tscan useconsole.*
Pino in client bundles:
- Use
@repo/utils/logger/clientin client code; never import@repo/utils/logger/serverin browser bundles - Inspect bundle analysis with
next build
Related Documentation
- Installation - Environment variable configuration
- Error Handling - Error logging with
@repo/sentry