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
| Environment | Format | Destination |
|---|---|---|
| Development | Pretty-print | stdout |
| Production | JSON | stdout |
Log Levels
| Level | When to Use |
|---|---|
| error | Something failed that shouldn't have |
| warn | Something unexpected but recoverable |
| info | Normal operations worth recording |
| debug | Detailed 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:
| Field | When | Example |
|---|---|---|
requestId | All request-scoped logs | c.get('requestId') |
siteId | Multi-tenant operations | User's site ID |
userId | User-initiated actions | Authenticated user ID |
err | Error objects | Caught exception |
duration_ms | Timed operations | How 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.