Logging

Comprehensive guide to logging in ShipKit

Logging

This document provides a comprehensive guide to logging in ShipKit, including setup, strategies, and best practices.

Logging Setup

Logger Configuration

// src/lib/logger.ts
import pino from 'pino'
import { env } from '@/env.mjs'

export const logger = pino({
  level: env.LOG_LEVEL || 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true,
      ignore: 'pid,hostname',
      translateTime: 'SYS:standard',
    },
  },
  base: {
    env: env.NODE_ENV,
    version: env.VERSION || '0.0.0',
  },
})

// Create child loggers for specific contexts
export function createLogger(context: string) {
  return logger.child({ context })
}

Environment Configuration

// src/env.mjs
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'

export const env = createEnv({
  server: {
    LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace'])
      .default('info'),
    LOG_FORMAT: z.enum(['json', 'pretty']).default('pretty'),
    LOG_OUTPUT: z.enum(['stdout', 'file']).default('stdout'),
    LOG_FILE_PATH: z.string().optional(),
  },
  // ...
})

Logging Patterns

Request Logging

// src/middleware.ts
import { NextResponse } from 'next/server'
import { type NextRequest } from 'next/server'
import { logger } from '@/lib/logger'

export async function middleware(request: NextRequest) {
  const requestLogger = logger.child({
    url: request.url,
    method: request.method,
    ip: request.ip,
    ua: request.headers.get('user-agent'),
  })

  const start = performance.now()

  try {
    const response = await NextResponse.next()
    const duration = performance.now() - start

    requestLogger.info({
      status: response.status,
      duration,
    }, 'Request completed')

    return response
  } catch (error) {
    requestLogger.error({
      error,
      duration: performance.now() - start,
    }, 'Request failed')

    throw error
  }
}

API Logging

// src/app/api/v1/posts/route.ts
import { NextResponse } from 'next/server'
import { createLogger } from '@/lib/logger'

const logger = createLogger('api:posts')

export async function GET(req: Request) {
  const { searchParams } = new URL(req.url)
  logger.debug({ params: Object.fromEntries(searchParams) }, 'Fetching posts')

  try {
    const posts = await db.post.findMany()
    logger.info({ count: posts.length }, 'Posts retrieved successfully')
    return NextResponse.json(posts)
  } catch (error) {
    logger.error({ error }, 'Failed to fetch posts')
    return new NextResponse('Internal Error', { status: 500 })
  }
}

Database Logging

// src/server/db/client.ts
import { PrismaClient } from '@prisma/client'
import { logger } from '@/lib/logger'

const dbLogger = logger.child({ context: 'prisma' })

export const db = new PrismaClient({
  log: [
    {
      emit: 'event',
      level: 'query',
    },
    {
      emit: 'event',
      level: 'error',
    },
    {
      emit: 'event',
      level: 'info',
    },
    {
      emit: 'event',
      level: 'warn',
    },
  ],
})

db.$on('query', (e) => {
  dbLogger.debug({
    query: e.query,
    params: e.params,
    duration: e.duration,
  }, 'Query executed')
})

db.$on('error', (e) => {
  dbLogger.error({
    target: e.target,
    message: e.message,
  }, 'Database error occurred')
})

db.$on('info', (e) => {
  dbLogger.info({
    message: e.message,
  }, 'Database info')
})

db.$on('warn', (e) => {
  dbLogger.warn({
    message: e.message,
  }, 'Database warning')
})

Error Logging

// src/lib/error-logger.ts
import { logger } from './logger'
import { type AppError } from './errors'

const errorLogger = logger.child({ context: 'error' })

export function logError(
  error: unknown,
  context?: Record<string, unknown>
) {
  if (error instanceof Error) {
    const errorInfo = {
      name: error.name,
      message: error.message,
      stack: error.stack,
      ...(error as AppError),
      ...context,
    }

    if (error instanceof AppError) {
      errorLogger.error(errorInfo, `Application error: ${error.code}`)
    } else {
      errorLogger.error(errorInfo, 'Unexpected error')
    }
  } else {
    errorLogger.error({ error, ...context }, 'Unknown error type')
  }
}

Log Management

Log Transport

// src/lib/log-transport.ts
import { pino } from 'pino'
import { env } from '@/env.mjs'

function createTransport() {
  if (env.LOG_OUTPUT === 'file') {
    return {
      target: 'pino/file',
      options: { destination: env.LOG_FILE_PATH },
    }
  }

  if (env.LOG_FORMAT === 'json') {
    return {
      target: 'pino/file',
      options: { destination: 1 }, // stdout
    }
  }

  return {
    target: 'pino-pretty',
    options: {
      colorize: true,
      ignore: 'pid,hostname',
      translateTime: 'SYS:standard',
    },
  }
}

export const transport = createTransport()

Log Rotation

// src/lib/log-rotation.ts
import { createStream } from 'rotating-file-stream'
import { env } from '@/env.mjs'

export const rotatingLogStream = createStream('application.log', {
  interval: '1d', // Rotate daily
  path: env.LOG_FILE_PATH,
  size: '10M', // Rotate when size exceeds 10MB
  compress: 'gzip', // Compress rotated files
  maxFiles: 7, // Keep logs for 7 days
})

Performance Logging

Timing Measurements

// src/lib/performance-logger.ts
import { logger } from './logger'

const perfLogger = logger.child({ context: 'performance' })

export class PerformanceTimer {
  private startTime: number
  private marks: Map<string, number>

  constructor(private name: string) {
    this.startTime = performance.now()
    this.marks = new Map()
  }

  mark(name: string) {
    this.marks.set(name, performance.now())
  }

  measure(name: string, startMark: string, endMark: string) {
    const start = this.marks.get(startMark)
    const end = this.marks.get(endMark)

    if (start && end) {
      perfLogger.info({
        operation: this.name,
        measurement: name,
        duration: end - start,
      }, 'Performance measurement')
    }
  }

  end() {
    const duration = performance.now() - this.startTime
    perfLogger.info({
      operation: this.name,
      duration,
    }, 'Operation completed')
  }
}

Resource Monitoring

// src/lib/resource-logger.ts
import { logger } from './logger'

const resourceLogger = logger.child({ context: 'resources' })

export function logResourceUsage() {
  const usage = process.memoryUsage()

  resourceLogger.info({
    heapTotal: usage.heapTotal / 1024 / 1024,
    heapUsed: usage.heapUsed / 1024 / 1024,
    rss: usage.rss / 1024 / 1024,
    external: usage.external / 1024 / 1024,
  }, 'Memory usage (MB)')
}

// Log resource usage every 5 minutes
if (env.NODE_ENV === 'production') {
  setInterval(logResourceUsage, 5 * 60 * 1000)
}

Best Practices

Log Levels

// src/lib/log-levels.ts
export const LogLevels = {
  FATAL: 60, // System is unusable
  ERROR: 50, // Error conditions
  WARN: 40,  // Warning conditions
  INFO: 30,  // Informational messages
  DEBUG: 20, // Debug messages
  TRACE: 10, // Trace messages
} as const

export type LogLevel = keyof typeof LogLevels

export function shouldLog(
  messageLevel: LogLevel,
  configLevel: LogLevel = 'INFO'
): boolean {
  return LogLevels[messageLevel] >= LogLevels[configLevel]
}

Structured Logging

// src/lib/structured-logger.ts
import { logger } from './logger'

interface LogContext {
  userId?: string
  requestId?: string
  action?: string
  resource?: string
  [key: string]: unknown
}

export function createStructuredLogger(defaultContext: LogContext = {}) {
  return {
    info(message: string, context: LogContext = {}) {
      logger.info({ ...defaultContext, ...context }, message)
    },
    error(error: Error | unknown, context: LogContext = {}) {
      logger.error({
        ...defaultContext,
        ...context,
        error: error instanceof Error ? {
          name: error.name,
          message: error.message,
          stack: error.stack,
        } : error,
      }, error instanceof Error ? error.message : 'Unknown error')
    },
    warn(message: string, context: LogContext = {}) {
      logger.warn({ ...defaultContext, ...context }, message)
    },
    debug(message: string, context: LogContext = {}) {
      logger.debug({ ...defaultContext, ...context }, message)
    },
  }
}

Sensitive Data Handling

// src/lib/sensitive-data.ts
const SENSITIVE_FIELDS = ['password', 'token', 'secret', 'key']

export function sanitizeData(
  data: Record<string, unknown>
): Record<string, unknown> {
  const sanitized: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(data)) {
    if (SENSITIVE_FIELDS.some((field) => key.toLowerCase().includes(field))) {
      sanitized[key] = '[REDACTED]'
    } else if (typeof value === 'object' && value !== null) {
      sanitized[key] = sanitizeData(value as Record<string, unknown>)
    } else {
      sanitized[key] = value
    }
  }

  return sanitized
}

Log Formatting

// src/lib/log-formatter.ts
import { format } from 'date-fns'

export function formatLogMessage(
  level: string,
  message: string,
  context: Record<string, unknown> = {}
): string {
  const timestamp = format(new Date(), 'yyyy-MM-dd HH:mm:ss.SSS')
  const contextStr = Object.entries(context)
    .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
    .join(' ')

  return `${timestamp} ${level.toUpperCase()} ${message} ${contextStr}`
}

Testing

Log Testing

// src/lib/__tests__/logger.test.ts
import { logger } from '../logger'
import { createStructuredLogger } from '../structured-logger'

describe('Logger', () => {
  let logs: any[] = []

  beforeEach(() => {
    logs = []
    jest.spyOn(logger, 'info').mockImplementation((obj, msg) => {
      logs.push({ level: 'info', obj, msg })
    })
  })

  afterEach(() => {
    jest.restoreAllMocks()
  })

  it('should log structured messages', () => {
    const structuredLogger = createStructuredLogger({
      service: 'test',
    })

    structuredLogger.info('Test message', { userId: '123' })

    expect(logs[0]).toEqual({
      level: 'info',
      obj: {
        service: 'test',
        userId: '123',
      },
      msg: 'Test message',
    })
  })
})

Mock Logger

// src/lib/__mocks__/logger.ts
export const mockLogger = {
  info: jest.fn(),
  error: jest.fn(),
  warn: jest.fn(),
  debug: jest.fn(),
}

export const logger = mockLogger