Logging

The built-in workspace logger (ws.logger) — six levels via LogLevel, runtime setLevel(), the Logger and ConsoleSink classes, and custom sinks through LogSink/LoggerConfig/SinkConfig.

Every workspace carries a logger. It is scoped to the workspace, its configuration is persisted alongside the workspace's own records, and it redacts high-confidence secrets before anything is written. You reach it through ws.logger once the workspace is unlocked, emit at one of six levels, and change the threshold or the destinations at runtime — changes are saved automatically.

The workspace logger

ws.logger returns a Logger bound to the namespace "wative". It is available after unlock — reading it while the workspace is locked throws WativeError("WORKSPACE_LOCKED"), because the persisted configuration cannot be read without the workspace key.

logger-basics.ts
import { Workspace } from "wative-core";

const ws = await Workspace.open({ path: "./wallet", password: "wsp-pwd" });

ws.logger.trace("trace event", { phase: "init" });
ws.logger.debug("debug event", { phase: "init" });
ws.logger.info("info event", { phase: "init" });
ws.logger.warn("warn event", { phase: "init" });
ws.logger.error("error event", { phase: "init" });

await ws.lock();

See Workspace.open for how a workspace is opened in Node versus the browser.

Log levels

Levels are ordered by severity. A record is emitted only when its level is at or above the logger's minLevel; everything below minLevel is discarded before any sink sees it.

LevelRankEmit methodTypical use
trace0trace(msg, ctx?)Most verbose; per-step detail.
debug1debug(msg, ctx?)Development diagnostics.
info2info(msg, ctx?)Normal lifecycle events (the default threshold).
warn3warn(msg, ctx?)Recoverable problems.
error4error(msg, err?, ctx?)A failed operation.
fatal5fatal(msg, err?, ctx?)Unrecoverable failure.

The LogLevel type is the union of these six names:

LogLevel
type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";

The Logger API

Logger is exported from wative-core. You normally use the instance handed back by ws.logger rather than constructing one.

MemberSignatureNotes
namespacereadonly stringThe logger's name; "wative" for the workspace logger.
levelLogLevel (getter)The current minimum level.
configLoggerConfig (getter)A fresh copy of the persisted configuration.
sinksreadonly LogSink[] (getter)A frozen copy of the live sink objects.
trace / debug / info / warn(msg: string, ctx?: Record<string, unknown>): voidEmit at that level.
error / fatal(msg: string, err?: unknown, ctx?: Record<string, unknown>): voidEmit with an optional error value.
setLevel(level: LogLevel): Promise<void>Change the threshold; persists.
setSinks(configs: SinkConfig[]): Promise<void>Replace the destinations; persists.
child(subNamespace: string): LoggerA sub-logger sharing the same config and sinks, with a dotted namespace.

Setting the level at runtime

setLevel updates the threshold and persists the new configuration. It resolves once the change is saved; the change only takes effect after the persist succeeds, so a rejected write never leaves an unpersisted level in force.

set-level.ts
await ws.logger.setLevel("warn");
ws.logger.config.minLevel; // "warn"

ws.logger.warn("still emitted");
ws.logger.info("below minLevel — discarded");

Config persistence

The logger configuration lives inside the workspace's LOG record. setLevel and setSinks write through to it, so the level and sinks you set survive a lock() / re-open() cycle.

persist.ts
let ws = await Workspace.open({ path: "./wallet", password: "wsp-pwd" });

await ws.logger.setLevel("error");
ws.logger.config.minLevel; // "error"

await ws.lock();

ws = await Workspace.open({ path: "./wallet", password: "wsp-pwd" });
ws.logger.config.minLevel; // "error" — the persisted level is restored

Sinks

A sink is where records go. The logger holds zero or more sinks; each record that passes minLevel is handed to every sink. Configure sinks with setSinks, passing an array of SinkConfig objects. Two sink kinds are recognized:

SinkConfig
type SinkConfig =
  | { kind: "console"; color?: boolean; minLevel?: LogLevel }
  | {
      kind: "file";
      dir: string;
      filePrefix?: string;
      maxFileSize?: number;
      maxFiles?: number;
      minLevel?: LogLevel;
    };

Each sink may carry its own minLevel, which is applied in addition to the logger's threshold — a sink never sees a record below the logger's minLevel, and can raise the bar further for itself.

attach-console-sink.ts
// Send info-and-above to the console, in color.
await ws.logger.setSinks([{ kind: "console", color: true }]);

ws.logger.info("now this is written to stdout");

// Stop all emission by clearing the sinks.
await ws.logger.setSinks([]);

ConsoleSink

ConsoleSink (exported from wative-core) writes formatted lines to process.stdout / process.stderr, falling back to console.log / console.error in environments without those streams (Electron renderers, browser bundles, web workers). ANSI color is stripped automatically when the stream is not a TTY.

ConsoleSink
new ConsoleSink(opts?: { color?: boolean; minLevel?: LogLevel });
// color defaults to true; minLevel defaults to "trace".

FileSink (Node)

The file sink is a rotating file writer and needs a filesystem, so it lives in wative-core/node. Importing that entry point registers the file-sink backend as a side effect; without it, a { kind: "file" } config is skipped rather than throwing — a persisted file sink must never make a workspace unopenable in the browser.

attach-file-sink.ts
import "wative-core/node"; // registers the filesystem backends (Node only)
import { Workspace } from "wative-core";

const ws = await Workspace.open({ path: "./wallet", password: "wsp-pwd" });

await ws.logger.setSinks([
  { kind: "file", dir: "./logs", filePrefix: "wallet", maxFiles: 7 },
]);

FileSink and its FileSinkOptions type are also exported from wative-core/node if you need to name them directly. The file config options:

OptionDefaultMeaning
dir(required)Directory the log files are written into (created with owner-only mode).
filePrefix"wative"Base file name. Must be a plain file name — a value containing a path separator throws WativeError("PARAMETER_ERROR").
maxFileSize10485760 (10 MiB)Rotate once the active file would exceed this many bytes. Must be a positive whole number.
maxFiles7How many rotated archives to keep. Must be a positive whole number.
minLevel"trace"Per-sink threshold.

Custom sinks — the LogSink interface

LogSink is the contract both built-in sinks implement, and the element type of logger.sinks. Implement it to write records wherever you like:

LogSink
interface LogSink {
  write(record: LogRecord): void;
  flush?(): Promise<void>;
  close?(): Promise<void>;
}

write is called synchronously for each record. close is awaited when the workspace locks. A sink that throws from write never crashes the caller — the logger swallows the error.

LogRecord

Every sink receives an immutable LogRecord. This is the shape after redaction has run — secrets in message, context, and error are already scrubbed.

LogRecord
interface LogRecord {
  readonly timestamp: number; // ms since the epoch
  readonly level: LogLevel;
  readonly logger: string;    // the emitting logger's namespace
  readonly message: string;
  readonly context?: Readonly<Record<string, unknown>>;
  readonly error?: { name: string; message: string; stack?: string };
}

LoggerConfig

The full persisted shape, returned by logger.config:

LoggerConfig
interface LoggerConfig {
  minLevel: LogLevel;
  sinks: SinkConfig[];
  captureStackOn?: ReadonlyArray<LogLevel>;
}
FieldDefaultMeaning
minLevel"info"Records below this level are discarded.
sinks[]The destinations. Empty means nothing is written.
captureStackOn["error", "fatal"]Levels for which a stack trace is captured from the err argument.

Automatic redaction

Before any sink writes, the logger scrubs high-confidence secrets from the message, the context object (deeply), and any error's message and stack:

  • Values under sensitive context keys (for example a key ending in privateKey, mnemonic, secret, password, token, or exactly pk / sk) become <redacted>.
  • URL credentialsscheme://user:pass@host — become scheme://<redacted>@host.
  • A run of twelve or more consecutive BIP-39 words becomes <redacted mnemonic>.

Last updated on