Logging
Putnami provides structured, context-aware logging via @putnami/runtime. Logs automatically include trace IDs, request context, and structured data — no manual wiring required.
Basic usage
import { useLogger } from '@putnami/runtime';
const logger = useLogger('orders');
logger.info('Order created', { orderId: '123' });
logger.warn('Low stock', { sku: 'WIDGET-01', remaining: 3 });
logger.error('Payment failed', error);useLogger() returns a context-aware logger. When called inside an HTTP request, the logger automatically includes the request's trace ID.
Log levels
Four levels, in order of severity:
| Level | Use for |
|---|---|
debug |
Development-only detail: variable values, branching decisions |
info |
Normal operations: request handled, job completed, record created |
warn |
Recoverable problems: deprecated usage, slow query, retry triggered |
error |
Failures: unhandled exceptions, external service down, data corruption |
The default level is info. Messages below the configured level are filtered out.
Named loggers
Create child loggers to organize output by concern:
const logger = useLogger('app');
const authLogger = logger.named('auth');
const dbLogger = logger.named('db');
authLogger.info('Token validated'); // logger: "app.auth"
dbLogger.warn('Slow query'); // logger: "app.db"Adding context
Use with() to attach structured data to all subsequent log entries:
const logger = useLogger('api');
logger.with('userId', 'usr_123');
logger.with('tenantId', 'acme');
logger.info('Request handled');
// → includes userId and tenantId in the log entryError logging
Pass Error objects directly — stack traces are extracted automatically:
try {
await processPayment(order);
} catch (error) {
logger.error('Payment processing failed', error);
}The log entry includes error.name, error.message, and error.stack as structured fields.
HTTP integration
The logger() plugin adds automatic request logging with trace ID propagation:
import { application, http, logger } from '@putnami/application';
const app = application()
.use(http({ port: 3000 }))
.use(logger({ exclude: ['/health'] }));Every request logs method, route, status code, and duration. Trace IDs are extracted from X-Cloud-Trace-Context or X-Correlation-ID headers, or generated automatically.
Output formats
JSON (default)
Structured JSON compatible with Google Cloud Logging. Each entry is a single JSON line with severity, message, timestamp, and context fields:
{"severity":"INFO","message":"Order created","timestamp":"2025-01-15T10:30:00.000Z","logger":"orders","traceId":"abc-123","orderId":"123"}Console
Human-readable text for local development:
[abc-123] [INFO] [orders] Order created { orderId: '123' }Configuration
Via environment variables:
LOG_LEVEL=debug # debug | info | warn | error (default: info)
LOGGER_JSON=false # true for JSON, false for console (default: true)Via YAML (conf/.env.local.yaml):
logger:
level: debug
json: falseBuffered logging
Group logs by request and flush together. Useful for high-throughput services:
logger:
buffer: true
bufferMaxSize: 100 # Entries per context before flush
bufferFlushInterval: 5000 # ms between flushesError-level entries flush the entire request context immediately.
Testing
Use MemoryLogger to capture and assert on log output:
import { MemoryLogger } from '@putnami/runtime';
const logger = new MemoryLogger('test');
logger.info('hello');
logger.error('failed', new Error('boom'));
expect(logger.entries).toHaveLength(2);
expect(logger.entries[0].message).toBe('hello');
expect(logger.entries[1].error?.name).toBe('Error');Global exception handling
Unhandled exceptions and rejected promises are captured automatically when the application starts. They log through the same logger with full context (trace ID, request data) when they occur inside a request.
Go
The Go logger follows the same patterns and output formats as the TypeScript runtime.
Basic usage
import "go.putnami.dev/logger"
log := logger.Default().Named("orders")
log.Info("Order created", slog.String("orderId", "123"))
log.Warn("Low stock", slog.String("sku", "WIDGET-01"), slog.Int("remaining", 3))
log.Error("Payment failed", err)Default() returns a shared logger configured from environment variables. Named() creates a child logger with an appended name.
Output formats
JSON output by default (same structure as TypeScript):
{"severity":"INFO","message":"Order created","timestamp":"2025-01-15T10:30:00.000Z","logger":"orders","orderId":"123"}Console output (same format as TypeScript):
[INFO] [orders] Order created orderId=123Configuration
Via environment variables:
LOG_LEVEL=debug # debug | info | warn | error (default: info)Context integration
log := logger.Default().Named("api")
reqLog := log.With("userId", "usr_123")
reqLog.Info("Request handled") // includes userId in log entry
// Attach to context
ctx = logger.WithLogger(ctx, reqLog)
logger.FromContext(ctx).Info("from context")Testing
sink := logger.NewMemorySink()
log := logger.New("test", logger.LevelDebug, sink)
log.Info("hello")
log.Error("failed", errors.New("boom"))
// sink.Len() == 2
// sink.Last().Message == "failed"
// sink.Last().Error.Message == "boom"