log
js/log.ts
fino:log — topic-based structured logging.
This module provides the application-facing logging API for fino backend
code. Loggers publish structured LogRecord objects onto fino:context/topic
channels; output is deliberately sink-driven. Creating a logger does not
install global stdout/stderr behavior, so libraries can safely log without
surprising their host application.
The default transport topic is "fino:log". Additional per-level and
per-logger topics are also published for callers that want narrower
subscriptions:
- fino:log
- fino:log:<level>
- fino:log:<logger-name>
Context is propagated with fino:context, so request IDs and similar fields
survive await boundaries. If an OpenTelemetry span is active, trace/span
identifiers are copied onto the record and can be forwarded with
createOtelSink().
import { createJsonSink, createLogger, withLogContext } from 'fino:log';
const sink = createJsonSink();
const log = createLogger({ name: 'api', level: 'info' });
{
using scope = withLogContext({ requestId: 'req-1' });
log.info('request started', { route: '/health' });
}
sink.dispose();
Types
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal'
Normalized severity level accepted by loggers and sinks.
Levels are ordered from most verbose to most severe. Logger thresholds and sink thresholds both use this ordering when deciding whether to emit or forward a record.
import type { LogLevel } from 'fino:log';
const level: LogLevel = 'info';
type Fields = Record<string, unknown>
Structured fields attached to log records.
Values should be JSON-friendly when records are sent to JSON or telemetry sinks, but the type intentionally accepts any value so custom sinks can decide how to render richer runtime objects.
import type { Fields } from 'fino:log';
const fields: Fields = { requestId: 'req-1' };
type LogContextScope = ContextScope
Disposable scope returned by withLogContext().
Dispose happens automatically when used with a using declaration, restoring
the previous async log context for the current execution branch. Capture the
scope explicitly only when you need to dispose it manually outside of a
using block.
import { withLogContext, type LogContextScope } from 'fino:log';
const scope: LogContextScope = withLogContext({ requestId: 'req-1' });
try {
// ... work that should log with requestId attached ...
} finally {
scope[Symbol.dispose]();
}
Interfaces
interface LogRecord {
Structured event emitted by Logger and consumed by sinks/subscribers.
Records are immutable by convention once published. Sinks receive the same record shape regardless of whether they render JSON, text, or OpenTelemetry log events.
import { createLogger, subscribeLogs, type LogRecord } from 'fino:log';
const events: LogRecord[] = [];
const sub = subscribeLogs((record) => events.push(record));
createLogger({ name: 'api' }).info('ready');
sub.dispose();
Properties
timestamp: string
ISO-8601 timestamp generated when the record is emitted.
const emittedAt = new Date(record.timestamp);
level: LogLevel
Normalized severity level.
if (record.level === 'error') {
// route to stderr
}
logger: string
Logger name, usually a dotted subsystem path.
const subsystem = record.logger;
message: string
Human-readable event message.
console.log(record.message);
fields: Fields
Per-call structured fields.
const route = record.fields.route;
context: Fields
Async-scoped and logger-scoped fields merged together.
const requestId = record.context.requestId;
error?: {
name: string;
message: string;
stack?: string;
}
Normalized error details when the message or fields.error is an Error.
if (record.error) {
console.error(record.error.message);
}
traceId?: string
OpenTelemetry trace id copied from the active span, when present.
const traceId = record.traceId ?? 'none';
spanId?: string
OpenTelemetry span id copied from the active span, when present.
const spanId = record.spanId ?? 'none';
traceFlags?: number
OpenTelemetry trace flags copied from the active span, when present.
const sampled = (record.traceFlags ?? 0) & 1;
interface LoggerOptions {
Options for constructing a Logger.
A logger name is required. The level controls which records are emitted, and context fields are attached to every record from that logger and its descendants.
import { Logger, type LoggerOptions } from 'fino:log';
const options: LoggerOptions = {
name: 'api',
level: 'info',
context: { service: 'users' },
};
const log = new Logger(options);
Properties
name: string
Non-empty logger name.
const options = { name: 'api' };
level?: LogLevel
Minimum emitted level. Defaults to trace.
const options = { name: 'api', level: 'warn' as const };
context?: Fields
Fields attached to every record emitted by this logger.
const options = { name: 'api', context: { service: 'users' } };
interface SinkOptions {
Shared options for log subscriptions and sinks.
The level threshold is applied by subscribers after records are published. It does not change logger-level filtering.
import { subscribeLogs, type SinkOptions } from 'fino:log';
const options: SinkOptions = { level: 'warn' };
const sub = subscribeLogs(() => {}, options);
sub.dispose();
Properties
level?: LogLevel
Minimum level accepted by the subscription. Defaults to all levels.
const options = { level: 'error' as const };
interface WriteSinkOptions extends SinkOptions {
Options for line-oriented sinks such as text and JSON output.
Provide write to capture lines in tests, send them to a custom stream, or
adapt records to a host logging system.
import { createJsonSink, type WriteSinkOptions } from 'fino:log';
const lines: string[] = [];
const options: WriteSinkOptions = { write: (line) => lines.push(line) };
const sink = createJsonSink(options);
sink.dispose();
Properties
write?: (line: string, record: LogRecord) => void
Receives each rendered line. Defaults to direct stdout/stderr writes.
const options = { write: (line: string) => console.log(line) };
Functions
function getLogContext(): Fields
Return the current async-scoped log context.
The returned object is a shallow copy, so mutating it does not alter the active context.
import { getLogContext, withLogContext } from 'fino:log';
{
using scope = withLogContext({ requestId: 'req-1' });
getLogContext().requestId; // 'req-1'
}
function withLogContext(context: Fields): LogContextScope
Enter a disposable structured log context scope.
The provided fields are shallow-merged over any existing context and are visible to logger calls made while the scope is active, including async continuations scheduled before the scope is disposed. Disposing restores the previous log context for the current execution branch.
import { createLogger, withLogContext } from 'fino:log';
const log = createLogger({ name: 'api' });
{
using scope = withLogContext({ requestId: 'req-1' });
await Promise.resolve();
log.info('handled request');
}
function runWithLogContext<R>(context: Fields, fn: () => R): R
Run fn with additional structured log context.
Prefer using scope = withLogContext(...) for new code. This callback helper
remains as a compatibility wrapper.
import { createLogger, runWithLogContext } from 'fino:log';
const log = createLogger({ name: 'api' });
await runWithLogContext({ requestId: 'req-1' }, async () => {
log.info('handled request');
});
function createLogger(options: LoggerOptions): Logger
Convenience factory for new Logger(options).
Use this when a factory reads more naturally than a constructor. It performs
the same validation and returns the same Logger class.
import { createLogger } from 'fino:log';
const log = createLogger({ name: 'api', level: 'info' });
log.info('ready');
function subscribeLogs(fn: (record: LogRecord) => void, options: SinkOptions = {})
Subscribe to structured log records.
Returns a disposable subscription handle. This is the lowest-level sink API; higher-level sinks are thin wrappers over this function.
import { createLogger, subscribeLogs } from 'fino:log';
const sub = subscribeLogs((record) => {
console.log(record.level, record.message);
}, { level: 'info' });
createLogger({ name: 'api' }).info('ready');
sub.dispose();
function createJsonSink(options: WriteSinkOptions = {})
Subscribe a JSON-lines sink.
Without a custom write, records are written directly to stdout/stderr with
one JSON object per line. The sink is opt-in; importing fino:log never
installs it automatically.
import { createJsonSink, createLogger } from 'fino:log';
const sink = createJsonSink({ level: 'info' });
createLogger({ name: 'api' }).info('ready');
sink.dispose();
function createConsoleSink(options: WriteSinkOptions = {})
Subscribe a human-readable console sink.
This is intended for local development. Production ingestion should normally
use createJsonSink() or createOtelSink().
import { createConsoleSink, createLogger } from 'fino:log';
const sink = createConsoleSink();
createLogger({ name: 'api' }).warn('slow request');
sink.dispose();
function createOtelSink(options: SinkOptions = {})
Subscribe an OpenTelemetry sink.
Records are forwarded through the active/default OTel LoggerProvider.
Context and fields become log attributes, and normalized errors become
exception.* attributes.
import { createLogger, createOtelSink } from 'fino:log';
const sink = createOtelSink({ level: 'info' });
createLogger({ name: 'api' }).info('exported to OpenTelemetry');
sink.dispose();
Classes
class Logger {
Emits structured log records onto the fino log topics.
A logger filters records by its configured level, merges logger context with async log context, and publishes matching records to broad and narrow topics.
import { Logger, createConsoleSink } from 'fino:log';
const sink = createConsoleSink({ level: 'info' });
const log = new Logger({ name: 'api', level: 'debug' });
log.info('ready', { port: 3000 });
sink.dispose();
Constructors
constructor(options: LoggerOptions)
Create a logger with a required non-empty name.
The constructor validates the level and shallow-copies context fields so later mutations of the options object do not affect emitted records.
import { Logger } from 'fino:log';
const log = new Logger({ name: 'api', level: 'info' });
Getters
get name(): string
Logger name included on every emitted record.
import { createLogger } from 'fino:log';
createLogger({ name: 'api' }).name; // 'api'
get level(): LogLevel
Minimum level this logger emits.
import { createLogger } from 'fino:log';
createLogger({ name: 'api', level: 'warn' }).level; // 'warn'
Methods
child(options: Fields | (Partial<LoggerOptions> & {
name?: string;
}) = {}): Logger
Create a child logger.
If options.name is provided, it is appended to the parent name with a
dot. Other fields become logger context unless provided under
options.context.
import { createLogger } from 'fino:log';
const root = createLogger({ name: 'api', context: { service: 'users' } });
const requests = root.child({ name: 'requests', route: '/users' });
requests.info('started');
log(level: LogLevel, message: unknown, fields: Fields = {}): void
Emit a record at an arbitrary level.
Prefer the level-specific helpers for normal use. Passing an Error as
message, or as fields.error, attaches normalized error details.
import { createLogger } from 'fino:log';
const log = createLogger({ name: 'api' });
log.log('warn', 'slow request', { durationMs: 1200 });
trace(message: unknown, fields: Fields = {}): void
Emit a trace-level record.
createLogger({ name: 'api' }).trace('cache lookup');
debug(message: unknown, fields: Fields = {}): void
Emit a debug-level record.
createLogger({ name: 'api' }).debug('cache miss', { key: 'user:1' });
info(message: unknown, fields: Fields = {}): void
Emit an info-level record.
createLogger({ name: 'api' }).info('request started');
warn(message: unknown, fields: Fields = {}): void
Emit a warn-level record.
createLogger({ name: 'api' }).warn('rate limit near capacity');
error(message: unknown, fields: Fields = {}): void
Emit an error-level record.
createLogger({ name: 'api' }).error(new Error('database unavailable'));
fatal(message: unknown, fields: Fields = {}): void
Emit a fatal-level record.
createLogger({ name: 'api' }).fatal('process cannot continue');