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.
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();The default configuration is minLevel: "info" with no sinks. With no sink attached, emit calls do no work and produce no output — the logger skips record construction entirely. Attach a sink (see Sinks) to see anything.
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.
| Level | Rank | Emit method | Typical use |
|---|---|---|---|
trace | 0 | trace(msg, ctx?) | Most verbose; per-step detail. |
debug | 1 | debug(msg, ctx?) | Development diagnostics. |
info | 2 | info(msg, ctx?) | Normal lifecycle events (the default threshold). |
warn | 3 | warn(msg, ctx?) | Recoverable problems. |
error | 4 | error(msg, err?, ctx?) | A failed operation. |
fatal | 5 | fatal(msg, err?, ctx?) | Unrecoverable failure. |
The LogLevel type is the union of these six names:
type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";error and fatal take an optional error value as their second argument, before the context object: error(msg: string, err?: unknown, ctx?: Record<string, unknown>). The other four take only (msg, ctx?). By default a stack trace is captured for error and fatal only (see captureStackOn under LoggerConfig).
The Logger API
Logger is exported from wative-core. You normally use the instance handed back by ws.logger rather than constructing one.
| Member | Signature | Notes |
|---|---|---|
namespace | readonly string | The logger's name; "wative" for the workspace logger. |
level | LogLevel (getter) | The current minimum level. |
config | LoggerConfig (getter) | A fresh copy of the persisted configuration. |
sinks | readonly LogSink[] (getter) | A frozen copy of the live sink objects. |
trace / debug / info / warn | (msg: string, ctx?: Record<string, unknown>): void | Emit at that level. |
error / fatal | (msg: string, err?: unknown, ctx?: Record<string, unknown>): void | Emit 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): Logger | A 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.
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.
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 restoredSinks
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:
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.
// 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.
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.
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:
| Option | Default | Meaning |
|---|---|---|
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"). |
maxFileSize | 10485760 (10 MiB) | Rotate once the active file would exceed this many bytes. Must be a positive whole number. |
maxFiles | 7 | How many rotated archives to keep. Must be a positive whole number. |
minLevel | "trace" | Per-sink threshold. |
File logging is Node-only. In the browser there is no filesystem — use a console sink, and see Environments for the Node-versus-browser split.
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:
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.
The persisted logger configuration recognizes only the console and file sink kinds. In wative-core 2.5.x there is no public API that installs an arbitrary LogSink instance into ws.logger; setSinks takes SinkConfig objects, not sink instances. LogSink is exported so you can type a sink you build and read logger.sinks.
LogRecord
Every sink receives an immutable LogRecord. This is the shape after redaction has run — secrets in message, context, and error are already scrubbed.
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:
interface LoggerConfig {
minLevel: LogLevel;
sinks: SinkConfig[];
captureStackOn?: ReadonlyArray<LogLevel>;
}| Field | Default | Meaning |
|---|---|---|
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 exactlypk/sk) become<redacted>. - URL credentials —
scheme://user:pass@host— becomescheme://<redacted>@host. - A run of twelve or more consecutive BIP-39 words becomes
<redacted mnemonic>.
Redaction deliberately does not mask bare hex or base58 strings: a private key, a transaction hash, a block hash, and a Solana signature are indistinguishable by shape, and masking them would hide the very values a support flow correlates on. Secrecy is decided by the context key name, not the value's shape — so log secrets under a clearly-named key ({ privateKey: "…" }), never as a bare positional string.