Toast
ContributorPatterns

Logging Patterns

Logging Patterns

Structured logging conventions for Toast.

Quick Reference

import { logger } from '../lib/logger.js';

logger.info({ userId, action }, 'User performed action');
logger.warn({ requestId, issue }, 'Potential problem detected');
logger.error({ err, context }, 'Operation failed');

Logger Setup

The logger is configured in apps/api/src/lib/logger.ts using Pino.

Output Format

EnvironmentFormatDestination
DevelopmentPretty-printstdout
ProductionJSONstdout

Log Levels

LevelWhen to Use
errorSomething failed that shouldn't have
warnSomething unexpected but recoverable
infoNormal operations worth recording
debugDetailed information for troubleshooting

Configure via LOG_LEVEL environment variable.

Request Logging

Every HTTP request is logged automatically by the request logger middleware:

{
  "level": "info",
  "time": 1234567890,
  "requestId": "abc-123",
  "method": "GET",
  "path": "/api/content",
  "status": 200,
  "duration_ms": 45,
  "msg": "Request completed"
}

Structured Logging

Always use structured data, not string interpolation:

// Good - structured data
logger.info({ userId, contentId, action: 'publish' }, 'Content published');

// Bad - string interpolation
logger.info(`User ${userId} published content ${contentId}`);

Standard Fields

Include these fields where applicable:

FieldWhenExample
requestIdAll request-scoped logsc.get('requestId')
siteIdMulti-tenant operationsUser's site ID
userIdUser-initiated actionsAuthenticated user ID
errError objectsCaught exception
duration_msTimed operationsHow long something took

Error Logging

For errors, pass the error object as err:

try {
  await riskyOperation();
} catch (error) {
  logger.error({ err: error, context: 'publishing' }, 'Publish failed');
  throw error;
}

Pino automatically extracts message and stack from error objects.

Sensitive Data

Never log:

  • Passwords or tokens
  • Full email addresses (use user@*** if needed)
  • Credit card numbers
  • Session tokens
  • API keys
// Bad
logger.info({ password, token }, 'Login attempt');

// Good
logger.info({ email: maskEmail(email) }, 'Login attempt');

Log Correlation

Use requestId to trace requests across services:

// The request ID is set by middleware and available via:
const requestId = c.get('requestId');

// Include in all logs within the request:
logger.info({ requestId, step: 'validation' }, 'Validating input');
logger.info({ requestId, step: 'database' }, 'Querying database');

Middleware Example

Creating new middleware with logging:

import { logger } from '../lib/logger.js';

export function myMiddleware(): MiddlewareHandler {
  return async (c, next) => {
    const requestId = c.get('requestId');

    logger.debug({ requestId }, 'Middleware starting');

    const start = Date.now();
    await next();
    const duration = Date.now() - start;

    logger.debug({ requestId, duration_ms: duration }, 'Middleware complete');
  };
}

Production Considerations

Log Aggregation

In production, logs go to stdout and are collected by Railway. Use structured JSON for easy querying.

Performance

  • Avoid logging in hot paths (tight loops)
  • Use appropriate log levels (debug for verbose, info for important)
  • Don't log large objects (request bodies, file contents)

Retention

Railway retains logs for a limited time. For audit requirements, consider shipping to a dedicated log service.

On this page