@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, andProvider(Logger)is the only way one is bound. There is nouseLoggerto reach past DI with. withreturns a logger; it never mutates. Nest'ssetContextwrites 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 whoseerrortook its cause second and whosewarntook none at all made a caller remember which arm it was in, and pushed every retryable failure up toerrorto keep its reason. - No
any, and no printf.Attributesis a flat record of scalars — the shape a log backend can index — and a failure goes incause, which the implementation normalises (anError'smessageandstackare non-enumerable, soJSON.stringifyalone 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
safeSinkapplies 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 itstraceId— 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
PortInstance<"Logger",LoggerService>
Constructors
Constructor
new Logger(): Logger;Defined in: di/dist/index.d.mts:15
Returns
Inherited from
Port("Logger")<LoggerService>.constructorProperties
| Property | Modifier | Type | Inherited from | Defined in |
|---|---|---|---|---|
[ID] | readonly | "Logger" | Port("Logger").[ID] | di/dist/index.d.mts:11 |
[SERVICE] | readonly | LoggerService | Port("Logger").[SERVICE] | di/dist/index.d.mts:12 |
portId | readonly | "Logger" | Port("Logger").portId | di/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
PortInstance<"LoggerConfig",LoggerSettings>
Constructors
Constructor
new LoggerConfig(): LoggerConfig;Defined in: di/dist/index.d.mts:15
Returns
Inherited from
Port("LoggerConfig")<LoggerSettings>.constructorProperties
| Property | Modifier | Type | Inherited from | Defined in |
|---|---|---|---|---|
[ID] | readonly | "LoggerConfig" | Port("LoggerConfig").[ID] | di/dist/index.d.mts:11 |
[SERVICE] | readonly | LoggerSettings | Port("LoggerConfig").[SERVICE] | di/dist/index.d.mts:12 |
portId | readonly | "LoggerConfig" | Port("LoggerConfig").portId | di/dist/index.d.mts:16 |
Type Aliases
Attributes
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
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
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
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
attributes | readonly | Attributes | - | observability/src/logger.ts:99 |
cause | readonly | unknown | - | observability/src/logger.ts:100 |
level | readonly | Level | - | observability/src/logger.ts:97 |
message | readonly | string | - | observability/src/logger.ts:98 |
time | readonly | number | Milliseconds since the epoch, stamped when the line was written. | observability/src/logger.ts:102 |
unit | readonly | | { tenantId?: string; traceId: string; unitId: string; } | undefined | What currentUnit() carried, or undefined outside a unit. | observability/src/logger.ts:104 |
LoggerService
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
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
debug | readonly | (message, attributes?, cause?) => void | - | observability/src/logger.ts:83 |
error | readonly | (message, attributes?, cause?) => void | cause is the failure itself — an Error, an unthrown Err's error, a rejected value. | observability/src/logger.ts:87 |
fatal | readonly | (message, attributes?, cause?) => void | - | observability/src/logger.ts:88 |
info | readonly | (message, attributes?, cause?) => void | - | observability/src/logger.ts:84 |
isEnabled | readonly | (level) => boolean | Whether a line at level would be written — for a payload expensive enough to be worth not building. | observability/src/logger.ts:92 |
log | readonly | (level, message, attributes?, cause?) => void | - | observability/src/logger.ts:81 |
trace | readonly | (message, attributes?, cause?) => void | - | observability/src/logger.ts:82 |
warn | readonly | (message, attributes?, cause?) => void | - | observability/src/logger.ts:85 |
with | readonly | (attributes) => LoggerService | A logger carrying attributes on every line it writes, on top of this one's. Never mutates this one. | observability/src/logger.ts:90 |
LoggerSettings
type LoggerSettings = object;Defined in: observability/src/config.ts:34
What observability() binds from the environment.
Properties
| Property | Modifier | Type | Defined in |
|---|---|---|---|
level | readonly | Level | observability/src/config.ts:34 |
ObservabilityOptions
type ObservabilityOptions = object;Defined in: observability/src/observability.ts:17
Properties
| Property | Modifier | Type | Description | Defined in |
|---|---|---|---|---|
level? | readonly | Level | Pins the level instead of reading LOG_LEVEL — a test's "fatal", a CLI's "debug". | observability/src/observability.ts:26 |
sink? | readonly | Sink | Where 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
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
| Parameter | Type |
|---|---|
line | Line |
Returns
void
Variables
LEVELS
const LEVELS: readonly Level[];Defined in: observability/src/logger.ts:14
The levels in order, least severe first — what isEnabled compares through.
Functions
createLogger()
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
| Parameter | Type | Default value |
|---|---|---|
sink | Sink | undefined |
level | Level | "info" |
Returns
jsonSink()
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
| Parameter | Type | Default value |
|---|---|---|
stream | { write: (chunk) => unknown; } | process.stdout |
stream.write | (chunk) => unknown | undefined |
Returns
kernelEvents()
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
| Parameter | Type |
|---|---|
logger | LoggerService |
Returns
EventSink
logLevel()
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
| Parameter | Type |
|---|---|
options | { default?: Level; } |
options.default? | Level |
Returns
ConfigField<Level>
observability()
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
| Parameter | Type |
|---|---|
options | ObservabilityOptions |
Returns
Module<Logger | LoggerConfig, ConfigInvalid, Env>