Skip to content

Logging

Stratal includes a built-in logging system with structured output, configurable log levels, and pluggable formatters. Each log entry is formatted and written to the console method that matches its level.

Pass logging options when creating your Stratal instance:

import { Stratal } from 'stratal'
import { AppModule } from './app.module'
export default new Stratal({
module: AppModule,
logging: {
level: 'info',
formatter: 'json',
},
})
OptionTypeDefaultDescription
level'debug' | 'info' | 'warn' | 'error''info'Minimum log level to output
formatter'json' | 'pretty''json'Output format

Stratal supports four log levels, ordered by priority:

LevelPriorityDescription
debug0Detailed diagnostic information for development
info1General operational events
warn2Unexpected situations that are not errors
error3Failures that need attention

The configured level acts as a minimum threshold. A level of info outputs info, warn, and error messages but suppresses debug. A level of error outputs only error messages.

Inject LoggerService using the LOGGER_TOKENS.LoggerService token:

import { Transient, inject } from 'stratal/di'
import { LOGGER_TOKENS, type LoggerService } from 'stratal/logger'
@Transient()
export class OrderService {
constructor(
@inject(LOGGER_TOKENS.LoggerService) private readonly logger: LoggerService,
) {}
async createOrder(userId: string, items: string[]) {
this.logger.info('Creating order', { userId, itemCount: items.length })
// ... create order logic
this.logger.debug('Order items', { items })
}
}

The logger provides four methods, one for each level:

logger.debug(message: string, context?: LogContext): void
logger.info(message: string, context?: LogContext): void
logger.warn(message: string, context?: LogContext): void
logger.error(message: string, error: Error, context?: LogContext): void
logger.error(message: string, context?: LogContext): void

Each method checks the configured level, formats the entry, and writes it to the console synchronously before returning.

Pass a LogContext object (a Record<string, unknown>) as the second argument to attach structured data to the log entry:

this.logger.info('Payment processed', {
orderId: 'order_123',
amount: 49.99,
currency: 'USD',
gateway: 'stripe',
})

The context is merged with internal metadata (like timestamp) and included in the formatted output.

The error method accepts either a LogContext object or an Error instance:

// With an Error object
try {
await database.query(sql)
} catch (error) {
this.logger.error('Database query failed', error)
}
// With an Error object and additional context
this.logger.error('Database query failed', error, { query: 'getUser' })
// With context only
this.logger.error('Payment declined', {
orderId: 'order_123',
reason: 'insufficient_funds',
})

When an Error is passed, the logger serializes its message, name, and stack into a structured error field on the log entry.

Formatters control how log entries are serialized to strings before being written to the console.

A formatter is any class implementing the ILogFormatter interface, which has a single method:

import type { LogEntry } from 'stratal/logger'
interface ILogFormatter {
format(entry: LogEntry): string
}

A LogEntry carries the level, message, the enriched context (your LogContext plus a timestamp), and an optional serialized error.

The default formatter for production. Outputs each log entry as a single JSON line:

{"level":"info","message":"Payment processed","timestamp":1708934400000,"orderId":"order_123","amount":49.99}

All context fields are spread into the top-level JSON object alongside level and message. The timestamp comes from the entry context. If an error is present, it appears as a nested error object.

Designed for local development. Outputs color-coded, human-readable logs:

[2026-02-26T10:00:00.000Z] INFO : Payment processed
orderId: "order_123"
amount: 49.99
currency: "USD"

Each level gets its own color:

LevelColor
debugCyan
infoGreen
warnYellow
errorRed

Error stack traces are printed below the context when present.

Switch to the pretty formatter in development:

export default new Stratal({
module: AppModule,
logging: {
level: 'debug',
formatter: 'pretty',
},
})

LoggerService formats each entry with the configured formatter and writes the result to the console method that matches its level:

LevelConsole method
debugconsole.debug()
infoconsole.info()
warnconsole.warn()
errorconsole.error()

On Cloudflare Workers these console calls are captured by the platform and surfaced through wrangler tail, the dashboard, and any Logpush or analytics integration you have wired up.

Formatters are the extension point for log output. To control the exact shape of what gets written, implement ILogFormatter and register your class on the LOGGER_TOKENS.Formatter token.

  1. Implement the ILogFormatter interface. The format method receives the full LogEntry and returns the string to write:

    import type { ILogFormatter, LogEntry } from 'stratal/logger'
    export class LogfmtFormatter implements ILogFormatter {
    format(entry: LogEntry): string {
    const fields: Record<string, unknown> = {
    level: entry.level,
    message: entry.message,
    ...entry.context,
    }
    if (entry.error) {
    fields.error = entry.error.message
    }
    return Object.entries(fields)
    .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
    .join(' ')
    }
    }
  2. Register it on the LOGGER_TOKENS.Formatter token from a module. A provider registered by your module takes precedence over the built-in json or pretty formatter:

    import { Module } from 'stratal/module'
    import { LOGGER_TOKENS } from 'stratal/logger'
    import { LogfmtFormatter } from './logfmt-formatter'
    @Module({
    providers: [
    { provide: LOGGER_TOKENS.Formatter, useClass: LogfmtFormatter },
    ],
    })
    export class AppModule {}

Every log entry now flows through your formatter before it is written to the console.