0Z4C0
// navigation
// @ozaco/std

Logger

A structured, level-based logger built on the plugin protocol, fanning entries out to installed transports.

import from@ozaco/std/logger

The logger is a protocol pair. Logger declares the logging contract — leveled methods (tracefatal), 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.

// additional entry points
  • @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`).
01

Protocols

The logging contract and the transport fan-out protocol.

const

Logger

The logger protocol — declares the leveled logging surface implemented by `DefaultLogger`.

signature
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.

example
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')
}
const

LoggerTransport

The transport protocol — a `cloneable`, fan-out sink whose actions run on every installed transport.

signature
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.

02

Logger

The default logger implementation.

const

DefaultLogger

The shipped `Logger` implementation; reads transports from the `LoggerTransport` registry.

signature
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 optionslevel (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.

example
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')
})
03

Transports

Built-in sinks, each imported from its own `transport/*` subpath and installed alongside the logger.

const

ConsoleTransport

Transport that formats entries and writes them to the console, routed by level.

signature
// 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.

example
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 console
ns

ConsoleDef

Option and context types for `ConsoleTransport`.

signature
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
  }
}
const

FileTransport

Transport that buffers NDJSON entries and appends them to a file via `@ozaco/std/io`.

signature
// 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.

example
import { install } from '@ozaco/std/plugin'
import { FileTransport } from '@ozaco/std/logger/transport/file'

yield* install(FileTransport, {
  path: './logs/app.ndjson',
  bufferSize: 64,
})
ns

FileDef

Option and context types for `FileTransport`.

signature
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
  }
}
04

Types

type

LoggerDef

The logger plugin shape and its associated option/context/entry/action types.

signature
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.
  • Payloadstring | Record<string, unknown> | Result<unknown, unknown> | undefined | null.
  • Entry — the normalized record: { level; time; msg; error; bindings; data }.
  • Actionslog, the leveled methods, child/bind, setLevel/isLevelEnabled, flush/close.
  • TransportEntry / AnyTransportPlugin — the transport-registration shapes.
type

LoggerTransportDef

The transport plugin shape and its context/action types.

signature
type LoggerTransportDef = Plugin<
  LoggerTransportDef.Context,
  unknown[],
  LoggerTransportDef.Actions
>

The LoggerTransportDef namespace also exposes:

  • Context{ name: string; level: LogLevel }.
  • Actionswrite(entry: LoggerDef.Entry), flush(), close().
05

Levels & constants

The log-level scale and the registered subtype tags.

const

LogLevel

Numeric severity scale; higher is more severe, and `silent` disables all output.

signature
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.

example
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)
const

LOGGER

Subtype tag identifying the logger protocol.

signature
const LOGGER: unique symbol // Symbol.for('std:logger')
const

LOGGER_TRANSPORT

Subtype tag identifying the transport protocol.

signature
const LOGGER_TRANSPORT: unique symbol // Symbol.for('std:logger:transport')