@absoluteui/services/logger
Structured logging with levels, sinks, sampling, and automatic redaction.
import { ... } from '@absoluteui/services/logger';How to use
Section titled “How to use”Structured logging for browser applications: levels, named child loggers, pluggable sinks, and automatic redaction of secrets. Messages are constant strings and data travels separately, so records are searchable and nothing sensitive is interpolated into text. Use it instead of scattered console calls when you want the same records in the console during development and at a telemetry endpoint in production.
How do I log structured records?
Section titled “How do I log structured records?”Create one logger for the application and pass data as an object. memorySink keeps records in memory, which is handy for tests and for a “copy diagnostics” button.
import { createLogger, memorySink } from '@absoluteui/services/logger';
const mem = memorySink({ capacity: 500 });const log = createLogger({ name: 'peoplehub', level: 'debug', context: { release: '2026.10' }, sinks: [mem] });log.info('Leave request submitted', { requestId: 'LR-1042', days: 3 });const [record] = mem.records();[record?.level, record?.message, record?.data]; // => ['info', 'Leave request submitted', { requestId: 'LR-1042', days: 3 }]record?.context; // => { release: '2026.10' }Without sinks, records go to the console (consoleSink(); pass { format: 'json' } for one JSON line per record).
How do I keep secrets and personal data out of logs?
Section titled “How do I keep secrets and personal data out of logs?”Values under secret-looking keys (password, token, authorization, cookie, session, API key, …) are replaced by '[REDACTED]', and query strings are stripped from values under keys ending in url, uri, or href. Add your own keys with redact.
import { DEFAULT_REDACT_KEYS } from '@absoluteui/helpers/sanitize';import { createLogger, memorySink } from '@absoluteui/services/logger';
const mem = memorySink();const log = createLogger({ sinks: [mem], redact: [...DEFAULT_REDACT_KEYS, 'nationalId', /^bank/i] });log.warn('Profile update failed', { employeeId: 42, nationalId: 'AB123456C', bankAccount: '12345678', accessToken: 'eyJhbGciOi', url: 'https://hr.example.com/api/profile?email=ada@example.com',});mem.records()[0]?.data; // => { employeeId: 42, nationalId: '[REDACTED]', bankAccount: '[REDACTED]', accessToken: '[REDACTED]', url: 'https://hr.example.com/api/profile' }How do I log errors?
Section titled “How do I log errors?”Pass the Error itself, or an object with an error field next to other data. It is serialized into record.error (name, message, code, status) without the stack unless includeStack is set.
import { ValidationError } from '@absoluteui/helpers/error';import { createLogger, memorySink } from '@absoluteui/services/logger';
const mem = memorySink();const log = createLogger({ name: 'payroll', sinks: [mem] });log.error('Payroll run failed', { runId: '2026-09', error: new ValidationError('PAYROLL_NO_BANK', 'Bank details are missing.') });const r = mem.records()[0];r?.data; // => { runId: '2026-09' }[r?.error?.name, r?.error?.code]; // => ['ValidationError', 'PAYROLL_NO_BANK']How do I give each part of the app its own name and context?
Section titled “How do I give each part of the app its own name and context?”child adds context (and optionally a dotted sub-name) and shares the parent’s sinks and level, so setLevel on the root changes every child.
import { createLogger, memorySink } from '@absoluteui/services/logger';
const mem = memorySink();const log = createLogger({ name: 'peoplehub', sinks: [mem] });const grid = log.child({ view: 'employees' }, { name: 'grid' });grid.debug('Rows rendered', { rows: 1200 }); // below the default 'info' level: droppedlog.setLevel('debug');grid.debug('Rows rendered', { rows: 1200 });mem.records().map((r) => [r.name, r.context]); // => [['peoplehub.grid', { view: 'employees' }]]withTimer(label) returns a function that logs label with durationMs when you call it.
How do I send logs to a server without flooding it?
Section titled “How do I send logs to a server without flooding it?”Sample noisy levels with sampleRate, keep only warnings and errors for the remote sink with filterSink, and batch them with batchingSink. Batches flush every few seconds and with navigator.sendBeacon when the page is hidden or closed.
import { batchingSink, consoleSink, createLogger, filterSink } from '@absoluteui/services/logger';
const remote = batchingSink( async (records) => { await fetch('/api/logs', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(records), keepalive: true }); }, { maxBatch: 50, flushIntervalMs: 10_000, beaconUrl: '/api/logs' },);export const log = createLogger({ name: 'peoplehub', sampleRate: { debug: 0.1, info: 0.5 }, // warn and error are always kept sinks: [consoleSink(), filterSink(remote, (r) => r.level === 'warn' || r.level === 'error')],});What goes wrong?
Section titled “What goes wrong?”- Never put data into the message (
`Saved ${email}`): redaction only seesdataandcontext. - A sink that throws is ignored, so logging never breaks the application;
batchingSinkdrops a failed batch (reported toonError) instead of retrying forever. - Unknown levels throw a
TypeError:createLogger({ level: 'verbose' })fails at start-up rather than silently logging nothing.
API reference
Section titled “API reference”Functions
Section titled “Functions”batchingSink
Buffers records and ships them in batches (for example to a telemetry endpoint). On pagehide or when the
page becomes hidden it sends the queue with navigator.sendBeacon (never blocking unload).
function batchingSink(send: (records: readonly LogRecord[]) => Promise<void> | void, options?: BatchingSinkOptions): BatchingSink;Example:
const remote = batchingSink((records) => fetch('/api/logs', { method: 'POST', body: JSON.stringify(records), keepalive: true }).then(() => undefined), { beaconUrl: '/api/logs' });consoleSink
Writes records to console (the only place this package uses it).
function consoleSink(options?: ConsoleSinkOptions): LogSink;Example:
createLogger({ sinks: [consoleSink({ format: 'json' })] });createLogger
Creates a structured logger.
function createLogger(options?: LoggerOptions): Logger;Example:
const mem = memorySink();const log = createLogger({ name: 'hr', level: 'debug', context: { app: 'PeopleHub' }, sinks: [mem] });log.info('Employee saved', { id: 42, token: 'x' });mem.records()[0]?.data; // => { id: 42, token: '[REDACTED]' }const end = log.withTimer('Load grid');end({ rows: 1200 }); // logs 'Load grid' with durationMs at debug levelfilterSink
Forwards only records that pass predicate (for example errors to a remote sink).
function filterSink(sink: LogSink, predicate: (record: LogRecord) => boolean): LogSink;Example:
const mem = memorySink();const log = createLogger({ sinks: [filterSink(mem, (r) => r.level === 'error' || r.level === 'warn')] });log.info('Loaded');log.warn('Retrying');mem.records().map((r) => r.message); // => ['Retrying']memorySink
Keeps the last capacity records in memory (tests, diagnostics, “copy logs” support buttons).
function memorySink(options?: { readonly capacity?: number;}): MemorySink;Example:
const mem = memorySink({ capacity: 200 });createLogger({ name: 'app', sinks: [mem] }).warn('Slow response');mem.records().map((r) => r.message); // => ['Slow response']Interfaces
Section titled “Interfaces”BatchingSink
Sink returned by {@link batchingSink}.
interface BatchingSink extends LogSink { flush(): Promise<void>; close(): Promise<void>; readonly dropped: number;}BatchingSinkOptions
Options for {@link batchingSink}.
interface BatchingSinkOptions { readonly maxBatch?: number; readonly flushIntervalMs?: number; readonly maxQueue?: number; readonly beaconUrl?: string; readonly onError?: (error: unknown, dropped: number) => void;}ConsoleSinkOptions
Options for {@link consoleSink}.
interface ConsoleSinkOptions { readonly format?: 'pretty' | 'json';}Logger
Logger returned by {@link createLogger}.
interface Logger { readonly name: string; readonly level: LogThreshold; setLevel(level: LogThreshold): void; isEnabled(level: LogLevel): boolean; trace(message: string, data?: unknown): void; debug(message: string, data?: unknown): void; info(message: string, data?: unknown): void; warn(message: string, data?: unknown): void; error(message: string, data?: unknown): void; child(context: Readonly<Record<string, unknown>>, options?: { readonly name?: string; }): Logger; withTimer(label: string, options?: { readonly level?: LogLevel; readonly data?: Readonly<Record<string, unknown>>; }): (data?: Readonly<Record<string, unknown>>) => number; flush(): Promise<void>;}LoggerOptions
Options for {@link createLogger}.
interface LoggerOptions { readonly level?: LogThreshold; readonly name?: string; readonly sinks?: readonly LogSink[]; readonly redact?: readonly (string | RegExp)[] | false; readonly stripUrlQuery?: boolean; readonly sampleRate?: number | Readonly<Partial<Record<LogLevel, number>>>; readonly context?: Readonly<Record<string, unknown>>; readonly includeStack?: boolean; readonly now?: () => number;}LogRecord
One structured log record.
interface LogRecord { readonly time: string; readonly level: LogLevel; readonly name: string; readonly message: string; readonly data?: unknown; readonly context: Readonly<Record<string, unknown>>; readonly error?: SerializedError;}LogSink
Destination for records. write must not throw (failures are swallowed).
interface LogSink { write(record: LogRecord): void; flush?(): Promise<void> | void; close?(): Promise<void> | void;}MemorySink
Ring-buffer sink returned by {@link memorySink}.
interface MemorySink extends LogSink { records(): LogRecord[]; clear(): void;}LogLevel
Severity of a record.
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';LogThreshold
Minimum level to emit; 'silent' emits nothing.
type LogThreshold = LogLevel | 'silent';