Docs / Packages / @zudojs/observability
v1.2.1

@zudojs/observability

Logs, metrics and traces for a Zudojs application, behind one small facade you can point at any backend.

OBSERVABILITY LOGGING METRICS TRACING EXPORTERS

OVERVIEW

When your program runs on your laptop you can read the screen. When it runs on a server you cannot. Observability is the practice of making a running program explain itself from the outside, and this package gives you the three standard ways to do that.

  • Structured logs are messages with named fields instead of one glued-together sentence. { "message": "order paid", "orderId": "o_9" } can be searched and filtered; "order o_9 paid" cannot.
  • Metrics are numbers measured over time — requests per minute, queue depth, response time. They answer "how much" and "how often", cheaply, forever.
  • Traces follow one request through every step it touches, so you can see which step was slow. A span is one of those steps: a named piece of work with a start time, an end time and some labelled facts attached.

This package does not talk to Datadog, Prometheus or OpenTelemetry itself. It defines the shapes — LogExporter, SpanExporter, MetricExporter — and ships console implementations. A provider package supplies a real one later, and your application code never changes.

USE IT WHEN

  • Your code runs somewhere you cannot watch it.
  • You need to know why one request out of a thousand was slow.
  • You want counts and latencies you can graph.
  • You are writing a library and want telemetry without picking a vendor for your users.

SKIP IT WHEN

  • A script runs once on your machine and prints its result.
  • Your host already collects everything you need and you are happy with plain console.log.
  • You are inside a unit test — use createNoopObservability() instead.

INSTALLATION

$ npm install @zudojs/observability

The runtime dependencies are @zudojs/errors and @zudojs/logger (whose secret-field rules the redactor reuses), which npm installs for you. Node 24 or newer is required — context propagation uses Node's built-in AsyncLocalStorage, so this package does not run in a browser.

These docs follow the framework source. If an export shown here is missing from the version you installed, update to the latest @zudojs release.

QUICK START

createObservability() builds one object holding a logger, a metrics registry, a tracer and a propagation manager. This is the only thing most applications create, and they create it once at startup.

This program logs a line, counts a request, times a span, and shuts everything down.

import { createObservability, LogLevel } from "@zudojs/observability"; const obs = createObservability({ serviceName: "orders-api", logLevel: LogLevel.INFO, // Redaction is on by default; see Redaction. }); obs.logger.info("server started", { port: 3000 }); obs.metrics.counter("http.requests", { route: "/orders" }).increment(); const span = obs.tracer.startSpan("load-orders"); span.setAttribute("customer.tier", "gold"); span.end(); // Pushes everything still buffered, then closes the exporters. await obs.shutdown();

What you should see. Three JSON lines on stdout: one log record, one span, and one metric snapshot. The log line looks like this (formatted here for reading; it prints on a single line):

{ "timestamp": "2026-09-09T10:15:00.412Z", "level": "info", "logger": "orders-api", "message": "server started", "context": { "port": 3000 } }

WATCH OUT

Printing to the console is the default, not a choice you made. useConsoleExporters defaults to true, so a deployed service will spray JSON at stdout until you pass real exporters or set it to false.

CHANGED IN 1.2.0

shutdown() exports the final metric snapshot once. Up to 1.1.x it exported it twice, with the same value and timestamp, so a backend that adds up counter exports counted the last interval double. If you added deduplication in your exporter for that, you can remove it. flush() is unchanged.

TIP

Always await obs.shutdown() before the process exits. Logs, spans and metrics are batched in memory, so anything not yet flushed is lost if you skip it. obs.flush() drains the buffers without closing anything.

LOGGING

A log level is a severity dial. Every message carries one, and the logger drops anything less severe than its threshold. LogLevel runs TRACE (0), DEBUG, INFO, WARN, ERROR, FATAL, OFF (6).

Every logging method takes the same three arguments: a message, an optional context object of named fields, and an optional error. Nothing is formatted into the message — the fields stay separate so a log tool can filter on them.

This creates a logger directly (no facade), writes at three levels, and shows what the threshold does.

import { createStructuredLogger, createConsoleLogExporter, createBatchLogProcessor, LogLevel, } from "@zudojs/observability"; // A transport is where records go. This one batches them into an exporter. const transport = createBatchLogProcessor({ exporter: createConsoleLogExporter(), }); const logger = createStructuredLogger({ name: "orders", level: LogLevel.INFO, transport, }); logger.debug("cache miss", { key: "o_9" }); // dropped: DEBUG < INFO logger.info("order paid", { orderId: "o_9", amount: 4200 }); logger.error("charge failed", { orderId: "o_9" }, new Error("card declined")); await logger.flush();

What you should see. Two lines, not three. The info line goes to console.log; the error line goes to console.error and carries an error field with the name, message and stack.

Child loggers

logger.child(name, context) returns a new logger whose name is parent.child and whose context is merged into every record it writes. Use it to stamp a request ID once instead of on every call.

const requestLog = logger.child("request", { requestId: "req_17" }); requestLog.info("handled", { status: 200 }); // logger: "orders.request", context: { requestId: "req_17", status: 200 }

WATCH OUT

A child copies the parent's level at the moment it is created. Calling setLevel() on the parent afterwards does not reach children that already exist.

IN PLAIN WORDS

Logging never throws. If a transport fails, the failure is swallowed rather than crashing the code that was trying to log — so a broken log sink shows up as silence, not as an error.

METRICS

There are three kinds of metric, and picking the right one is most of the work:

  • A counter only ever goes up, and counts how many times something happened.
  • A gauge goes up and down, and holds a current value like "connections open right now".
  • A histogram records many individual measurements and reports their spread — count, sum, min, max, and estimated percentiles.

Labels are the extra key/value pairs you pass alongside the name. Each distinct combination of name and labels is its own series, tracked separately.

This records all three kinds and prints what each one holds.

import { createMetricsRegistry } from "@zudojs/observability"; const registry = createMetricsRegistry({ maxSeries: 1000 }); const requests = registry.counter("http.requests", { route: "/orders" }); requests.increment(); requests.increment(5); console.log(requests.getValue()); // 6 const connections = registry.gauge("db.connections"); connections.setValue(10); connections.decrement(); console.log(connections.getValue()); // 9 const latency = registry.histogram("http.duration.ms"); latency.record(120); latency.record(340); latency.record(85); const spread = latency.getValue(); console.log(spread.count, spread.sum, spread.min, spread.max); // 3 545 85 340 console.log(registry.size()); // 3 — three series are registered

Percentiles are estimated from bucket boundaries, not from the raw values, so spread.p95 and latency.percentile(0.95) are approximations. The default boundaries are exported as DEFAULT_BUCKET_BOUNDARIES (1 through 10,000); pass your own through histogramBoundaries when your values live on a different scale.

Cardinality: the mistake that takes a backend down

Cardinality means how many distinct series a metric produces. A label holding a user ID, an order ID or a raw URL path produces one new series per value, forever. That is a memory leak in your process and a bill in your monitoring vendor.

The registry caps itself at 10,000 series by default. Past the cap it stops registering new ones and calls onCardinalityLimit with the offending metric name, once per rejected series; the facade's onError hears about each metric name once. Lower the cap and watch that callback.

import { createObservability } from "@zudojs/observability"; const obs = createObservability({ serviceName: "orders-api", useConsoleExporters: false, metrics: { maxSeries: 3 }, onError: (error, source) => console.error(source, String(error)), }); for (let i = 0; i < 100; i++) { obs.metrics.counter("by.path", { path: `/orders/${i}` }).increment(); } console.log(obs.metrics.size()); // 3 — the other 97 were refused await obs.shutdown();

What you should see. 3, plus repeated MetricsRegistry lines on stderr saying check for a high-cardinality label. The fix is the label, not the cap: use /orders/{id} as the route template.

DANGER

One metric name can only be one type. registry.counter("latency") followed by registry.histogram("latency") throws ObservabilityConfigError, because a backend that received both would reject the whole scrape.

WATCH OUT

counter.increment() throws MetricValueError on a negative, NaN or infinite value, and histogram.record() throws on NaN or infinity. A metric that silently ignored bad input would report a number nobody can reconcile with the code.

TRACING

A trace is the story of one request. A span is one chapter: a named piece of work with a start, an end, and attached facts. Spans nest — the span for "handle request" is the parent of the span for "query database".

Every span carries a context with a traceId (shared by every span in the request) and a spanId (unique to it). Passing a parent's context into startSpan links the two; with no parent, startSpan joins the active propagation context, and withSpan(tracer, name, fn) makes the span itself the active context.

This traces a request with one child step, records an error on the child, and exports both spans.

import { createObservability, SpanKind, SpanStatus, } from "@zudojs/observability"; const obs = createObservability({ serviceName: "orders-api" }); const parent = obs.tracer.startSpan("POST /orders", { kind: SpanKind.SERVER, attributes: { "http.route": "/orders" }, }); const child = obs.tracer.startSpan("db.insert", { parent: parent.context, kind: SpanKind.CLIENT, }); child.addEvent("retry", { attempt: 2 }); child.recordError(new Error("deadlock detected")); child.end(); parent.setStatus(SpanStatus.OK); parent.end(); console.log(child.context.traceId === parent.context.traceId); // true console.log(parent.getDuration() >= 0); // true (milliseconds) await obs.shutdown();

What you should see. true twice, then two JSON span lines. The child's parentSpanId equals the parent's spanId, its status is "ERROR", and it carries an "exception" event holding exception.type, exception.message and exception.stacktrace.

SpanKind says what the work was: INTERNAL (the default), SERVER, CLIENT, PRODUCER, CONSUMER. SpanStatus is UNSET, OK or ERROR; recordError() sets it to ERROR for you.

DANGER

A span you never end() is never exported and never freed. Put span.end() in a finally block so a thrown error cannot skip it.

IN PLAIN WORDS

Spans are capped so one runaway request cannot eat memory: 128 attributes, 128 events, 128 attributes per event, and attribute strings truncated at 4096 characters. Anything discarded is reported as droppedAttributes and droppedEvents on the exported span. Change the caps with spanLimits.

SAMPLING

Tracing every request in a busy service costs more than the service does. Sampling keeps a fraction of traces and drops the rest. A sampler makes that decision once, when the first span of a trace starts.

Four samplers ship in the box:

  • createAlwaysOnSampler() — keep everything. The default.
  • createAlwaysOffSampler() — keep nothing.
  • createProbabilitySampler(p) — keep roughly p of traces, decided from the trace ID so every service in the request agrees.
  • createParentBasedSampler(rootOrOptions?) — follow the incoming parent's decision, and use a root sampler only when there is no parent.

This keeps ten percent of new traces but always follows a decision already made upstream.

import { createObservability, createParentBasedSampler, createProbabilitySampler, } from "@zudojs/observability"; const obs = createObservability({ serviceName: "orders-api", sampler: createParentBasedSampler({ root: createProbabilitySampler(0.1), }), }); const span = obs.tracer.startSpan("GET /orders"); console.log(span.isRecording()); // true for ~10% of runs, false otherwise span.end(); await obs.shutdown();

What you should see. About one run in ten prints true and emits a span line; the rest print false and emit nothing. The span still has a real traceId either way, so child services stay correlated.

WATCH OUT

The probability is a fraction, not a percentage. Pass 0.1 for ten percent. A value outside 01 is silently clamped, so 10 quietly means "sample everything"; only NaN or infinity throws ObservabilityConfigError.

CONTEXT PROPAGATION

Passing a request ID down through twelve function calls is miserable. Propagation stores it once, at the top, and every function underneath can read it without being handed it — including across await. Node's AsyncLocalStorage does the work.

The stored value is a PropagationContext: traceId, spanId, and optional parentSpanId, requestId, correlationId, userId, service, traceFlags and baggage.

This runs a handler inside a context and shows the logger picking it up automatically.

import { createObservability, createPropagationContext, } from "@zudojs/observability"; const obs = createObservability({ serviceName: "orders-api" }); async function handle() { // Nothing was passed in, yet the ID is here. const current = obs.propagation.current(); console.log(current?.requestId); // "req_17" obs.logger.info("handled", { status: 200 }); } const context = createPropagationContext({ requestId: "req_17" }); await obs.propagation.run(context, handle); await obs.shutdown();

What you should see. req_17, then a log line that carries traceId and spanId fields you never wrote — the logger stamps them from the active context. Set correlate: false on a logger to turn that off.

Use runSync for synchronous work, and derive() to make a child context that keeps the trace ID and sampling flags but gets a fresh span ID.

WATCH OUT

current() returns undefined outside a run() scope. That is deliberate — inventing a context would make "no active trace" look like a real one. Use requireCurrentContext() only where starting a brand-new trace is an acceptable answer.

REDACTION

Redaction means replacing a sensitive value with a placeholder before it leaves the process. Telemetry is the classic accidental leak: a request body lands in a log context, the log ships to a vendor, and a password is now in someone else's database.

Redaction is on by default, as it is in @zudojs/logger. With no redaction option, every field name the logger redacts by default (password, passphrase, token, jwt, bearer, authorization, cookie, sid, pwd, API keys, card numbers and more) is redacted, along with this package's own DEFAULT_SENSITIVE_FIELDS. It is applied inside the logger and inside the span, so every processor and exporter downstream sees the already-redacted value.

CHANGED IN 1.2.0

Up to 1.1.x redaction was off unless you passed redaction, so obs.logger.info("login", { password }) exported the password in clear text. You no longer need redaction: {}; it still works and means the same as leaving the option out. To turn redaction off, pass redaction: false.

This uses the defaults and shows what they catch in both a log and a span.

import { createObservability } from "@zudojs/observability"; const obs = createObservability({ serviceName: "orders-api", // No redaction option: the default rules apply. redaction: false turns them off. }); obs.logger.info("login attempt", { email: "ada@example.com", userPassword: "hunter2", headers: { "x-api-key": "ak_live_42" }, }); const span = obs.tracer.startSpan("handle-request"); span.setAttribute("authorization", "Bearer sk-live-42"); span.setAttribute("http.route", "/orders/{id}"); span.end(); await obs.shutdown();

What you should see. The log context prints as { email: "ada@example.com", userPassword: "[REDACTED]", headers: { "x-api-key": "[REDACTED]" } }, and the span prints authorization: "[REDACTED]" while http.route is untouched.

What is covered, and what is not

DataRedacted?Notes
Log context fieldsYes, by defaultNested objects, arrays and instances of your own classes are walked too; built-ins such as Error, Date, Map and Set are left intact.
Span attributesYes, by defaultApplied on setAttribute, before any processor sees it.
Span event attributesYes, by defaultCovers addEvent.
The log message stringNoOnly named fields are examined. Never interpolate a secret into the message.
The error you pass to a log methodNoIts name, message and stack reach the exporter as written.
Error message and stack on a spanNorecordError stores them as strings under non-sensitive keys, so the field rules never fire. Set captureStackTraces: false to drop the stack.
Metric names and labelsNoKeep secrets and user IDs out of labels — see cardinality above.
Resource attributesNoYou write these once at startup; do not put credentials in them.

Tuning the rules

Matching is case-insensitive and, by default, word-aware. userPassword, x-api-key and accessToken all match; shippingAddress and authorId do not, even though they contain the letters pin and auth. Pass matchMode: "exact" to match whole names only.

This adds a field, a pattern and a custom rule on top of the defaults. fields, patterns and customRedactor all add to the default rules, so they can only redact more.

import { redactObject, type RedactionConfig } from "@zudojs/observability"; const config: RedactionConfig = { // fields and patterns both add to the defaults fields: ["nationalId"], patterns: [/^x-.*-token$/i], replacement: "***", customRedactor: (key, value) => key === "email" && typeof value === "string" ? value.replace(/^[^@]+/, "***") : value, }; console.log(redactObject({ email: "ada@example.com", nationalId: "A123", "x-refresh-token": "rt_1", jwt: "eyJhbGciOi", orderId: "o_9", }, config)); // { email: "***@example.com", nationalId: "***", // "x-refresh-token": "***", jwt: "***", orderId: "o_9" }

Traversal stops at depth 8 by default (raise it with maxDepth) and is cycle-aware. Anything cut off is replaced with MAX_DEPTH_MARKER or CIRCULAR_MARKER, both exported so you can recognise them.

DEFAULT_SENSITIVE_FIELDS is the whole default list: @zudojs/logger's DEFAULT_LOGGER_SECRET_FIELDS plus this package's extra spellings, frozen. You do not need to spread it into fields; fields: ["nationalId"] already redacts nationalId and every default name. To use your list alone, pass replaceDefaults: true:

import { redactObject } from "@zudojs/observability"; const input = { ssn: "078-05-1120", password: "hunter2", jwt: "eyJhbGciOi" }; console.log(redactObject(input, { fields: ["ssn"] })); // { ssn: "[REDACTED]", password: "[REDACTED]", jwt: "[REDACTED]" } console.log(redactObject(input, { fields: ["ssn"], replaceDefaults: true })); // { ssn: "[REDACTED]", password: "hunter2", jwt: "eyJhbGciOi" }

CHANGED IN 1.2.1 (BEHAVIOUR CHANGE, SECURITY)

Up to 1.2.0, fields replaced the default rules, and DEFAULT_SENSITIVE_FIELDS lacked jwt, sid, pwd, passphrase and bearer, so the old advice to write fields: [...DEFAULT_SENSITIVE_FIELDS, "nationalId"] left jwt in clear text. fields now extends the defaults, and that spread (or spreading DEFAULT_LOGGER_SECRET_FIELDS too) is harmless but no longer needed. If you relied on fields to redact less than the default, add replaceDefaults: true, and only for data you are sure is safe to export.

EXPORTERS & PROCESSORS

An exporter is the thing that actually sends telemetry somewhere. A processor sits in front of it and decides when: BatchSpanProcessor collects spans and ships them in groups, SimpleSpanProcessor ships each one immediately.

Batching exists because one network call per span is unaffordable. The cost is that a crash loses whatever is still queued — which is why shutdown() matters.

This writes spans into your own exporter instead of the console, and proves the span arrived.

import { createObservability, type ReadableSpan, type SpanExporter, } from "@zudojs/observability"; const collected: ReadableSpan[] = []; const exporter: SpanExporter = { async export(spans) { collected.push(...spans); }, async shutdown() {}, }; const obs = createObservability({ serviceName: "orders-api", useConsoleExporters: false, spanExporter: exporter, }); obs.tracer.startSpan("work").end(); await obs.shutdown(); console.log(collected.length, collected[0]?.name); // 1 work console.log(collected[0]?.resource["service.name"]); // orders-api

The same shape works for logs (LogExporter with export(records)) and metrics (MetricExporter with export(snapshots)). Metrics are pulled on a timer rather than pushed: metricExportIntervalMs defaults to 60,000, and 0 disables the timer while still exporting once on flush and shutdown.

Pass processors yourself when you want full control of batching. Do so and the facade stops building its own — which also means you own the exporter's lifecycle.

Resource attributes and scopes

Resource attributes describe who emitted the telemetry. serviceName, serviceVersion and environment become service.name, service.version and deployment.environment on every span.

const billing = obs.resource({ "module.name": "billing" }); billing.tracer.startSpan("charge").end(); // This span carries module.name; spans from `obs` do not.

WATCH OUT

A scope shares the parent's logger, registry and exporters — only the span resource differs. shutdown() on a scope is a no-op; shut down the root instance instead. flush() on a scope does work.

CONFIGURATION

Every field of ObservabilityConfig. Only serviceName is required.

FieldWhat it doesDefault
serviceNameNames the service on every log and span.required
serviceVersion, environmentExtra resource attributes.omitted
logLevelThreshold for the facade's logger.LogLevel.INFO
logExporter, spanExporter, metricExporterWhere each signal is sent.console, or noop when console exporters are off
useConsoleExportersFall back to console exporters when none is supplied.true
samplerWhich traces to record.AlwaysOnSampler
processorsYour own span processors. Supplying them disables the built-in batch processor.one BatchSpanProcessor
redactionRedaction rules for log contexts and span attributes (RedactionConfig), or false to turn redaction off.on, with the default rules
metricsMetricsRegistryOptions: maxSeries, histogramBoundaries, onCardinalityLimit.cap 10,000
spanLimitsPer-span caps on attributes, events and string length.128 / 128 / 128 / 4096
resourceExtra attributes stamped on every span.{}
metricExportIntervalMsHow often metrics are exported. 0 disables the timer.60000
logFlushIntervalMs, logBatchSizeLog buffering behaviour.1000 / 256
captureStackTracesRecord exception.stacktrace on spans. Stacks are not redacted.true
onErrorCalled with (error, source) for telemetry failures that are otherwise swallowed.none

TIP

Set onError in every real deployment. Without it a failing exporter, a full queue or a cardinality blow-up is completely silent.

API REFERENCE

Everything below is exported from @zudojs/observability. Most applications only need createObservability and the four things it hands back.

Functions

NameWhat it doesNotes
createObservability(config)Builds the facade: logger, metrics, tracer, propagation.The main entry point. Returns DefaultObservability.
createNoopObservability()A facade that records nothing.For tests and for libraries with no telemetry configured.
createStructuredLogger(options)A logger on its own, without the facade.Takes LoggerOptions.
createMetricsRegistry(options?)A metrics registry on its own.Takes MetricsRegistryOptions.
createCounter, createGauge, createHistogramSingle, unregistered metric instruments.(name, labels?); histogram also takes boundaries?.
createPeriodicMetricReader(options)Snapshots a registry on a timer and exports it.The facade builds one for you.
metricKey(type, name, labels?)The cache key for one series.Useful when mirroring the registry's identity rules.
createTracer(options?)A tracer on its own.Takes TracerOptions, including redactAttribute.
createSpan(name, options?)A span without a tracer.No sampling, no processors.
createSpanContext, createChildSpanContext, isSampledContextBuild and inspect span contexts; IDs are validated and an invalid parent starts a fresh trace (isValidSpanContext).For wiring trace headers by hand.
createAlwaysOnSampler, createAlwaysOffSampler, createProbabilitySampler(p), createParentBasedSampler(rootOrOptions?)The four built-in samplers.p is a fraction in 01.
isSampled(result), isRecording(result)Read a SamplingResult.For custom samplers.
createPropagationContext, derivePropagationContext, createPropagationManagerBuild contexts and the manager that stores them.The facade exposes a manager as obs.propagation.
getCurrentContext(), requireCurrentContext()Read the active context.The first may return undefined; the second invents a new trace.
createConsoleLogExporter, createConsoleSpanExporter, createConsoleMetricExporterPrint telemetry as JSON.Take ConsoleExporterOptions (pretty, console).
createBatchSpanProcessor, createSimpleSpanProcessor, createBatchLogProcessorBuffer or forward telemetry to an exporter.Batch options: batchSize, flushIntervalMs, maxQueueSize, onError, onDrop.
createRedactor(config?), createStructureRedactor(config?)Redact one field, or walk a whole structure.Used internally unless redaction is false.
redactObject(object, config?), redactValue(value, config?), isSensitiveField(key, config?)One-shot redaction helpers.Handy in tests and in your own sinks.
generateTraceId(), generateSpanId(), isValidTraceId, isValidSpanIdW3C-shaped IDs: 32 and 16 hex characters.Applied by every context factory: an invalid inbound ID starts a fresh trace. Use parseTraceparent / formatTraceparent for W3C headers.
logLevelToName, logLevelFromName, parseLogLevel, shouldLog, getLogLevelNamesConvert between levels and names.parseLogLevel returns undefined on junk — good for env vars.
createLogRecord, createErrorLogRecord, serializeErrorBuild records and serialise thrown values.For custom transports.
safeStringify(value, pretty?)JSON.stringify that cannot throw.Handles cycles, BigInt, functions, symbols, errors.
isObservabilityError(value)Type guard for this package's errors.Use in catch blocks.

Classes

NameWhat it doesNotes
DefaultObservabilityThe facade implementation.Adds resource(), flush(), shutdown().
StructuredLoggerThe logger implementation.Adds child(), setLevel(), flush().
DefaultMetricsRegistryIn-memory registry with the series cap.Throws on a name reused as another type.
DefaultCounter, DefaultGauge, DefaultHistogramThe three instruments.Reject non-finite values.
PeriodicMetricReaderTimer that collects and exports snapshots.start(), collect(), shutdown().
DefaultTracer, DefaultSpanTracer and span implementations.Tracer adds forceFlush(), shutdown(), exportSpan().
AlwaysOnSampler, AlwaysOffSampler, ProbabilitySampler, ParentBasedSamplerSampler implementations.Prefer the create* factories.
AsyncPropagationManagerContext storage backed by AsyncLocalStorage.run, runSync, current, derive.
ConsoleSpanExporter, ConsoleLogExporter, ConsoleMetricExporterJSON-to-stdout exporters.Log records at ERROR and above go to console.error.
BatchSpanProcessor, SimpleSpanProcessor, BatchLogProcessorBuffering and forwarding.Batch versions expose getQueueSize() and getDroppedCount().
NoopObservabilityFacade that discards everything.Paired with noopLogger, noopTracer and friends.

Types & enums

NameWhat it doesNotes
LogLevel, LogLevelNameSeverity, numeric and by name.TRACE 0 … OFF 6.
SpanStatus, SpanKind, TraceFlagsSpan outcome, role, and the sampling bit.TraceFlags.SAMPLED is 1.
ObservabilityConfig, ObservabilityWhat you pass in and what you get back.See Configuration above.
Logger, LoggerOptions, LogRecord, LogRecordError, LogTransportThe logging contract.Implement LogTransport for a custom sink.
Counter, Gauge, Histogram, HistogramValue, MetricsRegistry, MetricSnapshotThe metrics contract.HistogramValue carries p50p99 and cumulative buckets.
MetricsRegistryOptions, PeriodicMetricReaderOptionsRegistry and reader tuning.metrics in the config takes the former.
Span, SpanContext, SpanEvent, SpanOptions, SpanLimits, ReadableSpan, Tracer, TracerOptionsThe tracing contract.TracerOptions.redactAttribute redacts every attribute a span records.
Sampler, SamplingResult, ParentBasedSamplerOptionsThe sampling contract.Decision is one of three strings.
PropagationContext, PropagationContextOptions, PropagationManagerThe propagation contract.current() may be undefined.
SpanExporter, LogExporter, MetricExporter, SpanProcessorWhat a backend package implements.Two methods each: export and shutdown.
ConsoleExporterOptions, ConsoleLikeConsole exporter tuning.console lets tests capture output.
BatchSpanProcessorOptions, BatchLogProcessorOptionsBatching tuning.Include onDrop for queue overflow.
RedactionConfig, RedactionMatchModeRedaction rules."contains" (default) or "exact".

Errors

NameThrown whenNotes
ObservabilityErrorBase class for everything below (defined in @zudojs/errors and re-exported).Test with isObservabilityError(value).
ObservabilityConfigErrorA name is reused as another metric type, or a sampler probability is out of range.A programming mistake — fix the call, do not catch it.
MetricValueErrorA metric receives a negative, NaN or infinite value, or invalid histogram boundaries.Carries the metric name and the offending value.
ExporterErrorAn exporter fails.Surfaces through onError rather than at your call site.

Constants & singletons

NameWhat it isNotes
DEFAULT_SENSITIVE_FIELDSThe effective default redaction field list.The logger's DEFAULT_LOGGER_SECRET_FIELDS plus this package's extra spellings; frozen. fields adds to it unless replaceDefaults: true.
DEFAULT_BUCKET_BOUNDARIESDefault histogram buckets, 1 to 10,000.Tuned for millisecond latencies.
CIRCULAR_MARKER, MAX_DEPTH_MARKERPlaceholders left by the redactor."[CIRCULAR]" and "[MAX_DEPTH]".
INVALID_PROPAGATION_CONTEXTAll-zero context used by the noop manager.Frozen.
noopLogger, noopCounter, noopGauge, noopHistogram, noopMetricsRegistry, noopSpan, noopTracer, noopPropagationManagerDo-nothing implementations of each interface.Useful as defaults in library code.
noopSpanExporter, noopLogExporter, noopMetricExporterExporters that discard everything.What the facade uses when console exporters are off.

COMMON MISTAKES

  • Exiting without await obs.shutdown(). Buffered logs, spans and the final metric snapshot are dropped, and the process may hang on the metric timer. Await shutdown() in your termination handler.
  • Assuming every secret is stripped. Redaction covers named fields in log contexts and span attributes. Log messages, error messages and stacks, and metric labels are not covered, so keep secrets out of them. And never ship redaction: false outside a local debugging session.
  • Putting an ID in a metric label. { path: "/orders/9182" } creates one series per order. You hit the 10,000-series cap, new series are refused, and your graphs go flat. Use the route template.
  • Deploying with the console exporters. useConsoleExporters defaults to true, so production writes JSON to stdout. Pass real exporters or set it to false.
  • Forgetting span.end() on the error path. The span is never exported and never released. End it in a finally.
  • Reaching for replaceDefaults: true to add a name. Since 1.2.1, redaction: { fields: ["myToken"] } adds myToken to the defaults. replaceDefaults: true throws the defaults away, so password stops being redacted. Leave it off unless you mean to redact less.
  • Leaving onError unset. Dropped batches, failing exporters and cardinality warnings all go nowhere. Wire it to your own logger.

COMPLETE EXPORT INDEX

Every name @zudojs/observability exports from its package root at v1.1.1 — 142 in total, generated from the package’s own entry point rather than written by hand. The sections above explain the ones you reach for most; this is the exhaustive list, so nothing shipped is undocumented. Names not covered above are typically internal helpers and supporting types.

Show all 142 exports
Classes (25)
AlwaysOffSampler AlwaysOnSampler AsyncPropagationManager BatchLogProcessor BatchSpanProcessor ConsoleLogExporter ConsoleMetricExporter ConsoleSpanExporter DefaultCounter DefaultGauge DefaultHistogram DefaultMetricsRegistry DefaultObservability DefaultSpan DefaultTracer ExporterError MetricValueError NoopObservability ObservabilityConfigError ObservabilityError ParentBasedSampler PeriodicMetricReader ProbabilitySampler SimpleSpanProcessor StructuredLogger
Functions (56)
createAlwaysOffSampler createAlwaysOnSampler createBatchLogProcessor createBatchSpanProcessor createChildSpanContext createConsoleLogExporter createConsoleMetricExporter createConsoleSpanExporter createCounter createErrorLogRecord createGauge createHistogram createLogRecord createMetricsRegistry createNoopObservability createObservability createParentBasedSampler createPeriodicMetricReader createProbabilitySampler createPropagationContext createPropagationManager createRedactor createSimpleSpanProcessor createSpan createSpanContext createStructuredLogger createStructureRedactor createTracer derivePropagationContext formatTraceparent fromLoggerLevel generateSpanId generateTraceId getCurrentContext getLogLevelNames isObservabilityError isRecording isSampled isSampledContext isSensitiveField isValidSpanContext isValidSpanId isValidTraceId logLevelFromName logLevelToName metricKey parseLogLevel parseTraceparent redactObject redactValue requireCurrentContext safeStringify serializeError shouldLog toLoggerLevel withSpan
Interfaces (38)
BatchLogProcessorOptions BatchSpanProcessorOptions ConsoleExporterOptions ConsoleLike Counter Gauge Histogram HistogramValue LogExporter Logger LoggerOptions LogRecord LogRecordError LogTransport MetricExporter MetricSnapshot MetricsRegistry MetricsRegistryOptions Observability ObservabilityConfig ParentBasedSamplerOptions PeriodicMetricReaderOptions PropagationContext PropagationContextOptions PropagationManager ReadableSpan RedactionConfig Sampler SamplingResult Span SpanContext SpanEvent SpanExporter SpanLimits SpanOptions SpanProcessor Tracer TracerOptions
Type aliases (2)
LogLevelName RedactionMatchMode
Constants (18)
CIRCULAR_MARKER DEFAULT_BUCKET_BOUNDARIES DEFAULT_SENSITIVE_FIELDS INVALID_PROPAGATION_CONTEXT MAX_DEPTH_MARKER noopCounter noopGauge noopHistogram noopLogExporter noopLogger noopMetricExporter noopMetricsRegistry noopPropagationManager noopSpan noopSpanExporter noopTracer TraceFlags TRACEPARENT_HEADER
Enums (3)
LogLevel SpanKind SpanStatus