Logger
A structured, level-based logger built on the plugin protocol, fanning entries out to installed transports.
@ozaco/std/loggerThe logger is a protocol pair. Logger declares the logging contract — leveled methods (trace…fatal), child/bind for contextual bindings, setLevel/isLevelEnabled, and flush/close — while LoggerTransport is a fan-out protocol every sink implements. DefaultLogger is the shipped implementation: install it, install one or more transports, then log through Logger.actions.*.
Each log call builds a normalized Entry (level, time, message, error, bindings, data) and dispatches it. Because LoggerTransport overrides its exec strategy, a single write/flush/close fans out to every installed transport concurrently — there is no separate registry, transports are tracked simply by being installed.
Everything is an effect operation (generator + yield*), so logging composes with the rest of std: transports themselves use @ozaco/std/io and @ozaco/std/effect under the hood. Bindings set via bind or scoped with child are merged into every entry emitted within that scope.
@ozaco/std/logger/transport/consoleProvides `ConsoleTransport` and the `ConsoleDef` option/context types — a transport that formats each entry (pretty, colorized by default, or NDJSON when `pretty: false`) and routes it by level to `console.error`/`warn`/`info`/`debug`.@ozaco/std/logger/transport/fileProvides `FileTransport` and the `FileDef` option/context types — a transport that serializes entries to NDJSON, buffers them, and appends to `options.path` via `@ozaco/std/io`, draining when the buffer reaches `bufferSize` (and on `flush`/`close`).
Protocols
The logging contract and the transport fan-out protocol.
Logger
The logger protocol — declares the leveled logging surface implemented by `DefaultLogger`.
const Logger: Protocol<
LoggerDef.Context,
[options?: LoggerDef.Options],
LoggerDef.Actions
>
// Logger.actions run against the installed logger implementation:
Logger.actions.log(level: LogLevel, ...args: LoggerDef.Payload[]): Future<void>
Logger.actions.trace(...args: LoggerDef.Payload[]): Future<void>
Logger.actions.debug(...args: LoggerDef.Payload[]): Future<void>
Logger.actions.info(...args: LoggerDef.Payload[]): Future<void>
Logger.actions.warn(...args: LoggerDef.Payload[]): Future<void>
Logger.actions.error(...args: LoggerDef.Payload[]): Future<void>
Logger.actions.fatal(...args: LoggerDef.Payload[]): Future<void>
Logger.actions.child<R>(
bindings: Record<string, unknown>,
fn: () => Operation<R>,
): Future<R>
Logger.actions.bind(bindings: Record<string, unknown>): Future<void>
Logger.actions.setLevel(level: LogLevel): Future<void>
Logger.actions.isLevelEnabled(level: LogLevel): Future<boolean>
Logger.actions.flush(): Future<void>
Logger.actions.close(): Future<void>Defined with defineProtocol (name logger, subtype LOGGER). A Payload is a string, a plain object, a Result, undefined or null — strings become the message, objects merge into data, and Result failures are unwrapped into the error field. Levels below the configured level are dropped before an entry is built.
import { DefaultLogger, Logger, LogLevel } from '@ozaco/std/logger'
import { ConsoleTransport } from '@ozaco/std/logger/transport/console'
import { install } from '@ozaco/std/plugin'
function* program() {
yield* install(ConsoleTransport)
yield* install(DefaultLogger, { level: LogLevel.debug })
yield* Logger.actions.info('server started', { port: 5173 })
yield* Logger.actions.error({ err: new Error('boom') }, 'request failed')
}LoggerTransport
The transport protocol — a `cloneable`, fan-out sink whose actions run on every installed transport.
const LoggerTransport: Protocol<
LoggerTransportDef.Context,
unknown[],
LoggerTransportDef.Actions
>
// LoggerTransport.actions fan out to ALL installed transports, concurrently:
LoggerTransport.actions.write(entry: LoggerDef.Entry): Future<void>
LoggerTransport.actions.flush(): Future<void>
LoggerTransport.actions.close(): Future<void>Declared cloneable so multiple transports can be installed side by side, each with its own context. Its exec override runs every installed transport concurrently (all(entries.map(run))), which is how one logger dispatch reaches all sinks. Implement it (via LoggerTransport.implement(...).build(...)) to build custom sinks; ConsoleTransport and FileTransport are the built-ins.
Logger
The default logger implementation.
DefaultLogger
The shipped `Logger` implementation; reads transports from the `LoggerTransport` registry.
const DefaultLogger: LoggerDef
// = Logger.implement({ name: 'default-logger', version: '0.0.1', setup }).build({
// log, trace, debug, info, warn, error, fatal,
// child, bind, setLevel, isLevelEnabled, flush, close,
// })On setup it seeds context from options — level (default LogLevel.info), timestamp (default Date.now), errorKey (default err), msgKey (default msg) — and installs any initial bindings. Each log builds an Entry and dispatches it to LoggerTransport.actions.write; flush/close forward to the transports. child runs fn with extra bindings scoped to that operation, while bind mutates the current bindings.
import { DefaultLogger, Logger, LogLevel } from '@ozaco/std/logger'
import { install } from '@ozaco/std/plugin'
yield* install(DefaultLogger, {
level: LogLevel.debug,
bindings: { service: 'api' },
})
// Scope extra bindings to a sub-operation.
yield* Logger.actions.child({ requestId: 'r-1' }, function* () {
yield* Logger.actions.info('handling request')
})Transports
Built-in sinks, each imported from its own `transport/*` subpath and installed alongside the logger.
ConsoleTransport
Transport that formats entries and writes them to the console, routed by level.
// from '@ozaco/std/logger/transport/console'
const ConsoleTransport: Plugin<
ConsoleDef.Context,
[options?: ConsoleDef.Options],
LoggerTransportDef.Actions
>By default pretty is on and color is auto-detected, so entries are rendered human-readably; set pretty: false for NDJSON, or pass a custom format. write skips entries below the transport level (defaults to the logger level), then routes to console.error (>= error), console.warn (>= warn), console.info (>= info) or console.debug. flush/close are no-ops.
import { install } from '@ozaco/std/plugin'
import { ConsoleTransport } from '@ozaco/std/logger/transport/console'
yield* install(ConsoleTransport) // pretty + color
yield* install(ConsoleTransport, { pretty: false }) // NDJSON to consoleConsoleDef
Option and context types for `ConsoleTransport`.
namespace ConsoleDef {
interface Options {
level?: LogLevel
pretty?: boolean // default: true
color?: boolean // default: auto-detected
msgKey?: string // default: 'msg'
errorKey?: string // default: 'err'
format?: (entry: LoggerDef.Entry) => Operation<string>
}
interface Context {
name: string
level: LogLevel
format: (entry: LoggerDef.Entry) => Operation<string>
options: Options
}
}FileTransport
Transport that buffers NDJSON entries and appends them to a file via `@ozaco/std/io`.
// from '@ozaco/std/logger/transport/file'
const FileTransport: Plugin<
FileDef.Context,
[options: FileDef.Options],
LoggerTransportDef.Actions
>On setup it ensures options.path exists (via IO.actions.ensureFile) and defaults the level to LogLevel.trace. write serializes each entry to NDJSON, pushes it into an in-memory buffer, and drains (appending with IO_FLAGS.APPEND) once the buffer reaches bufferSize. flush and close both drain the remaining buffer.
import { install } from '@ozaco/std/plugin'
import { FileTransport } from '@ozaco/std/logger/transport/file'
yield* install(FileTransport, {
path: './logs/app.ndjson',
bufferSize: 64,
})FileDef
Option and context types for `FileTransport`.
namespace FileDef {
interface Options {
path: string
level?: LogLevel
msgKey?: string // default: 'msg'
errorKey?: string // default: 'err'
ensureDir?: boolean
bufferSize?: number // entries buffered before a drain
format?: (entry: LoggerDef.Entry) => Operation<string>
}
interface Context {
name: string
level: LogLevel
buffer: string[]
limit: number
format: (entry: LoggerDef.Entry) => Operation<string>
options: Options
}
}Types
LoggerDef
The logger plugin shape and its associated option/context/entry/action types.
type LoggerDef = Plugin<
LoggerDef.Context,
[options?: LoggerDef.Options],
LoggerDef.Actions
>The LoggerDef namespace also exposes:
- •
Options—{ level?; bindings?; msgKey?; errorKey?; timestamp? }passed on install. - •
Context—{ level; msgKey; errorKey; timestamp }held per logger. - •
Payload—string | Record<string, unknown> | Result<unknown, unknown> | undefined | null. - •
Entry— the normalized record:{ level; time; msg; error; bindings; data }. - •
Actions—log, the leveled methods,child/bind,setLevel/isLevelEnabled,flush/close. - •
TransportEntry/AnyTransportPlugin— the transport-registration shapes.
LoggerTransportDef
The transport plugin shape and its context/action types.
type LoggerTransportDef = Plugin<
LoggerTransportDef.Context,
unknown[],
LoggerTransportDef.Actions
>The LoggerTransportDef namespace also exposes:
- •
Context—{ name: string; level: LogLevel }. - •
Actions—write(entry: LoggerDef.Entry),flush(),close().
Levels & constants
The log-level scale and the registered subtype tags.
LogLevel
Numeric severity scale; higher is more severe, and `silent` disables all output.
enum LogLevel {
trace = 10,
debug = 20,
info = 30,
warn = 40,
error = 50,
fatal = 60,
silent = Number.POSITIVE_INFINITY,
}A call is emitted only when its level is >= the logger context level, so setting the level to LogLevel.warn drops trace/debug/info. silent is +Infinity, above every real level, so it suppresses everything.
import { LogLevel } from '@ozaco/std/logger'
LogLevel.info >= LogLevel.debug //=> true (info would be emitted at level debug)
LogLevel.debug >= LogLevel.warn //=> false (debug is dropped at level warn)LOGGER
Subtype tag identifying the logger protocol.
const LOGGER: unique symbol // Symbol.for('std:logger')LOGGER_TRANSPORT
Subtype tag identifying the transport protocol.
const LOGGER_TRANSPORT: unique symbol // Symbol.for('std:logger:transport')