Logging
Pino is the recommended first-class logger for autotel. Bunyan and Winston also work via OpenTelemetry auto-instrumentation.
Pino (Recommended)
Section titled “Pino (Recommended)”Pino is first-class: pass a single instance to init() for both autotel internal logs and app logs.
import pino from 'pino';import { init, trace, withTracing } from 'autotel';
const logger = pino({ name: 'my-app', level: 'info', transport: { target: 'pino-pretty', options: { colorize: true }, },});
// One logger for autotel + app logsinit({ service: 'my-app', logger, endpoint: process.env.OTLP_ENDPOINT,});
const createUser = withTracing({})((ctx) => async (name: string) => { logger.info({ name }, 'Creating user'); // trace_id, span_id automatically injected into log output const user = await db.users.create({ name }); logger.info({ userId: user.id }, 'User created'); return user;});Log Output
Section titled “Log Output”{ "level": 30, "time": 1700000000000, "msg": "Creating user", "name": "Alice", "trace_id": "a1b2c3d4e5f6", "span_id": "1234567890ab"}Bunyan
Section titled “Bunyan”Use auto-instrumentation for trace context injection:
import { init } from 'autotel';
init({ service: 'my-app', autoInstrumentations: ['bunyan'],});import bunyan from 'bunyan';
const logger = bunyan.createLogger({ name: 'my-app' });
// trace_id, span_id automatically added to every log recordlogger.info({ userId: '123' }, 'Processing request');Winston
Section titled “Winston”import { init } from 'autotel';
init({ service: 'my-app', autoInstrumentations: ['winston'],});import winston from 'winston';
const logger = winston.createLogger({ transports: [new winston.transports.Console()],});
// trace_id, span_id automatically addedlogger.info('Processing request');Request Logger vs Pino
Section titled “Request Logger vs Pino”They serve different purposes:
| Feature | getRequestLogger() |
Pino/Bunyan/Winston |
|---|---|---|
| Purpose | One wide event per request | Continuous structured logging |
| Output | Attributes on the span | Log stream (stdout/files) |
| When | .emitNow() at end of request |
Throughout request |
| Use case | Canonical log lines / wide events | Debugging, audit trails |
Use both together: Pino for continuous logging + getRequestLogger() for the one snapshot at the end of each request.
Snapshot severity
Section titled “Snapshot severity”The request snapshot carries an autotel.log.level attribute. Calling
.warn() sets it to warn and .error() sets it to error, so most requests
never need you to touch it.
Two cases need you to say it out loud. A request that degraded without logging anything, and a request that logged a warning you do not want colouring the whole snapshot:
const log = getRequestLogger(ctx);
const inventory = await checkStock(sku);if (inventory.stale) { log.setLevel('warn'); // served from a stale cache, nothing to log}setLevel() takes 'debug' | 'info' | 'warn' | 'error', adds no log event, and
records no exception. An explicit level wins: a later .warn() or .error()
leaves your choice alone rather than overwriting it. Calls after .emitNow()
are ignored, because the snapshot has already left.
Use it to make severity searchable when the interesting thing is the outcome rather than a message.
Canonical log lines: logger and OTLP at once
Section titled “Canonical log lines: logger and OTLP at once”canonicalLogLines emits one wide line per span. Where it goes is the part
worth getting right.
With no logger, lines go through the OpenTelemetry Logs API and out to
whichever OTLP logs backend you configured — Loki and the like. Set a logger
and they go there instead, which is easy to do by accident: the top-level
logger on init() is the fallback, so setting that alone also diverts them
away from OTLP.
On a platform whose log view reads stdout you usually want both:
init({ service: 'my-app', endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, canonicalLogLines: { enabled: true, logger: pino(), otel: true },});otel defaults to true only when no logger is given. Setting otel: true
explicitly also wires the endpoint’s log exporter for you, unless logs: false
(or AUTOTEL_LOGS=off) says otherwise — otherwise the lines would be written to
a no-op provider and never arrive.
logger also takes an array, to fan the same line out to several destinations.
Examples
Section titled “Examples”example-pino: Pino as first-class logger withinit({ logger })and trace context injection.example-bunyan: Bunyan with auto-instrumentation.example-winston: Winston with auto-instrumentation.