Skip to content

@btravstack/observability


@btravstack/observability / index

index

Classes

Logger

Defined in: observability/src/logger.ts:64

The application's logger, as a port.

Deliberately unlike NestJS's Logger, and each difference is a defect this shape does not have:

  • A port, not a class. Nothing is newed, nothing is static, and nothing is global: a test provides its own, and Provider(Logger) is the only way one is bound. There is no useLogger to reach past DI with.
  • with returns a logger; it never mutates. Nest's setContext writes to the instance every caller shares, so two request scopes racing it interleave each other's context. A child here is a value.
  • One argument order, and every level can carry a failure. Six methods, one shape: (message, attributes?, cause?). A logger whose error took its cause second and whose warn took none at all made a caller remember which arm it was in, and pushed every retryable failure up to error to keep its reason.
  • No any, and no printf. Attributes is a flat record of scalars — the shape a log backend can index — and a failure goes in cause, which the implementation normalises (an Error's message and stack are non-enumerable, so JSON.stringify alone loses exactly the part worth keeping).
  • It cannot throw. A logger that throws turns an observability problem into an outage; every implementation this package ships swallows its own failures, the same rule the kernel's safeSink applies to an event sink.
  • Correlation is not the caller's job. The default implementation reads currentUnit() per call, so every line inside a unit carries its traceId — and reading it per call rather than at construction is what makes one application-scope logger correct for every unit.

Synchronous void, not an AsyncResult: a log call is fire-and-forget by definition — a caller who awaited it would be waiting on I/O to decide nothing — and this package's thesis-6 exemption is exactly that. Delivery is the implementation's problem, and a lost line is not a modeled error.

Extends

Constructors

Constructor
ts
new Logger(): Logger;

Defined in: di/dist/index.d.mts:15

Returns

Logger

Inherited from
ts
Port("Logger")<LoggerService>.constructor

Properties

PropertyModifierTypeInherited fromDefined in
[ID]readonly"Logger"Port("Logger").[ID]di/dist/index.d.mts:11
[SERVICE]readonlyLoggerServicePort("Logger").[SERVICE]di/dist/index.d.mts:12
portIdreadonly"Logger"Port("Logger").portIddi/dist/index.d.mts:16

LoggerConfig

Defined in: observability/src/observability.ts:15

What the graph bound from the environment: the level every logger in it filters at. A port of its own, like a starter's HttpConfig, so anything that wants to know reads it rather than re-deriving it.

Extends

Constructors

Constructor
ts
new LoggerConfig(): LoggerConfig;

Defined in: di/dist/index.d.mts:15

Returns

LoggerConfig

Inherited from
ts
Port("LoggerConfig")<LoggerSettings>.constructor

Properties

PropertyModifierTypeInherited fromDefined in
[ID]readonly"LoggerConfig"Port("LoggerConfig").[ID]di/dist/index.d.mts:11
[SERVICE]readonlyLoggerSettingsPort("LoggerConfig").[SERVICE]di/dist/index.d.mts:12
portIdreadonly"LoggerConfig"Port("LoggerConfig").portIddi/dist/index.d.mts:16

Type Aliases

Attributes

ts
type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;

Defined in: observability/src/logger.ts:27

What a line carries besides its message: a flat record of scalars.

Flat and scalar on purpose. A structured line is queried by field in the system that receives it, and a nested object is where a field's name stops being stable (user.id on one line, user: { id } on another); an unknown value is where a logger starts stringifying whatever it is handed, which is how a log call becomes the thing that throws. Anything else is the caller's to render — and a failure has a channel of its own, cause, which the implementation normalises.


Level

ts
type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal";

Defined in: observability/src/logger.ts:11

The severity of one line, and the whole of the set: six levels, ordered, with no silly, no verbose and no caller-defined additions. A fixed set is what lets LOG_LEVEL be validated at startup, isEnabled be a comparison rather than a lookup, and a future OpenTelemetry bridge map each one to a severity number without a table of synonyms.


Line

ts
type Line = object;

Defined in: observability/src/logger.ts:96

One line, as the implementation hands it to a Sink: the message, its severity, and everything known about it.

Properties

PropertyModifierTypeDescriptionDefined in
attributesreadonlyAttributes-observability/src/logger.ts:99
causereadonlyunknown-observability/src/logger.ts:100
levelreadonlyLevel-observability/src/logger.ts:97
messagereadonlystring-observability/src/logger.ts:98
timereadonlynumberMilliseconds since the epoch, stamped when the line was written.observability/src/logger.ts:102
unitreadonly| { tenantId?: string; traceId: string; unitId: string; } | undefinedWhat currentUnit() carried, or undefined outside a unit.observability/src/logger.ts:104

LoggerService

ts
type LoggerService = object;

Defined in: observability/src/logger.ts:80

Every method takes the same three arguments in the same order, including the six that name their own level: (message, attributes?, cause?).

Uniform on purpose, and it was not at first. error(message, cause, attributes) read better at the call site that always has a cause and made every OTHER call site remember which arm it was in — and it left warn with nowhere to put one, so a retryable failure (a broker that refused a publish, which comes back) had to be logged at error purely to keep the reason. A failure is not a property of severity: an info line reporting a recovered fault carries one too. The cost is logger.error("boom", undefined, cause) for a failure with nothing else to say, which is rare — a line worth writing almost always has an id to write with it.

Properties

PropertyModifierTypeDescriptionDefined in
debugreadonly(message, attributes?, cause?) => void-observability/src/logger.ts:83
errorreadonly(message, attributes?, cause?) => voidcause is the failure itself — an Error, an unthrown Err's error, a rejected value.observability/src/logger.ts:87
fatalreadonly(message, attributes?, cause?) => void-observability/src/logger.ts:88
inforeadonly(message, attributes?, cause?) => void-observability/src/logger.ts:84
isEnabledreadonly(level) => booleanWhether a line at level would be written — for a payload expensive enough to be worth not building.observability/src/logger.ts:92
logreadonly(level, message, attributes?, cause?) => void-observability/src/logger.ts:81
tracereadonly(message, attributes?, cause?) => void-observability/src/logger.ts:82
warnreadonly(message, attributes?, cause?) => void-observability/src/logger.ts:85
withreadonly(attributes) => LoggerServiceA logger carrying attributes on every line it writes, on top of this one's. Never mutates this one.observability/src/logger.ts:90

LoggerSettings

ts
type LoggerSettings = object;

Defined in: observability/src/config.ts:34

What observability() binds from the environment.

Properties

PropertyModifierTypeDefined in
levelreadonlyLevelobservability/src/config.ts:34

ObservabilityOptions

ts
type ObservabilityOptions = object;

Defined in: observability/src/observability.ts:17

Properties

PropertyModifierTypeDescriptionDefined in
level?readonlyLevelPins the level instead of reading LOG_LEVEL — a test's "fatal", a CLI's "debug".observability/src/observability.ts:26
sink?readonlySinkWhere lines go. Default: one JSON object per line on stdout — dependency-free, and the shape every log backend already reads. The @btravstack/observability/pino subpath is the same seam for a deployment that wants pino's throughput.observability/src/observability.ts:24

Sink

ts
type Sink = (line) => void;

Defined in: observability/src/logger.ts:110

Where a line goes. Given a Line, writes it — and never throws, which createLogger guarantees on its behalf.

Parameters

ParameterType
lineLine

Returns

void

Variables

LEVELS

ts
const LEVELS: readonly Level[];

Defined in: observability/src/logger.ts:14

The levels in order, least severe first — what isEnabled compares through.

Functions

createLogger()

ts
function createLogger(sink, level?): LoggerService;

Defined in: observability/src/logger.ts:127

A logger over sink, filtered at level and correlated with the ambient unit.

The correlation is read per call, not captured: one logger is built per scope and every unit the kernel opens has its own record, so a logger that captured it at construction would stamp the wrong trace id on every line but the first. with layers attributes and nothing else, so a child costs one object and shares the sink.

Every path is wrapped: a sink that throws is swallowed here, because a logger that takes the process down is worse than a line nobody sees.

Parameters

ParameterTypeDefault value
sinkSinkundefined
levelLevel"info"

Returns

LoggerService


jsonSink()

ts
function jsonSink(stream?): Sink;

Defined in: observability/src/json-sink.ts:40

One JSON object per line on stream, the shape every log backend already reads and the same one the kernel's stderrSink writes its events in.

The caller's attributes are spread first and the line's own fields after them, and that order is the precedence: an attributes: { level: "info" } cannot rewrite an error line's severity, nor its traceId, because the sink writes those last. A stream where a caller can forge the severity is a stream nobody can trust.

The unit's ids are spread at the top level rather than nested under unit: a log backend indexes fields, and traceId is the field an operator searches.

Parameters

ParameterTypeDefault value
stream{ write: (chunk) => unknown; }process.stdout
stream.write(chunk) => unknownundefined

Returns

Sink


kernelEvents()

ts
function kernelEvents(logger): EventSink;

Defined in: observability/src/observability.ts:79

The kernel's nine lifecycle events, as log lines on logger.

StartOptions.onEvent takes a sink and the kernel's default writes JSON to stderr, which is correct for a process with no logger and wrong for one with: two streams, two shapes, two sets of fields to search. This is the adapter between them — pass it as onEvent and serving lands next to the request that was in flight when it did.

The mapping is deliberate rather than mechanical. startFailed and uncaught are error: they carry a cause and they are what an operator is paged for. teardownError is warn — the application is already stopping and the exit code says so — and everything else is info, one line per transition. The event's own fields become attributes, so draining keeps its inFlight count and drained its report.

The logger is passed in rather than resolved: this runs before the graph exists (building is emitted while it is still being built), so it cannot come from the context it is watching.

Parameters

ParameterType
loggerLoggerService

Returns

EventSink


logLevel()

ts
function logLevel(options?): ConfigField<Level>;

Defined in: observability/src/config.ts:16

LOG_LEVEL, as a ConfigField of the six levels and nothing else.

A value outside the set is a ConfigInvalid naming the variable — exit 78 under runMain, before a line is written — rather than a silent fallback to info: a deployment that meant debug and typed verbose should be told, not quietly under-logged for a week. Built on Config.string, so it inherits the semantics every other variable has: an unset variable takes the default, a set-but-blank one is an error.

Parameters

ParameterType
options{ default?: Level; }
options.default?Level

Returns

ConfigField<Level>


observability()

ts
function observability(options?): Module<Logger | LoggerConfig, ConfigInvalid, Env>;

Defined in: observability/src/observability.ts:45

The observability starter: a module providing the application's Logger and the LoggerConfig it was built from.

Import it next to the application and export Logger — that is the whole of it. Every line carries the ambient unit's traceId because the logger reads currentUnit() per call, so a request's lines are attributable without a single argument threaded through the call stack, and without the mutable per-instance context that makes that trick unsafe elsewhere.

An application that wants its own implementation provides Logger itself and does not import this module; one that wants this implementation with a different destination passes a sink. Both are the same seam a starter always offers: the default behaviour is here, and it is one argument to replace.

Parameters

ParameterType
optionsObservabilityOptions

Returns

Module<Logger | LoggerConfig, ConfigInvalid, Env>

Released under the MIT License.