ILogger
Last updated April 7, 2026
Structured logging interface with child loggers and optional log buffering.
ILogger is the interface every logging adapter implements. Unlike most platform interfaces, all methods are synchronous (return void, not Promise<void>) — logging is fire-and-forget, and adapters that persist logs flush asynchronously in the background.
Source of truth: platform/kb-labs-core/packages/core-platform/src/adapters/logger.ts.
Interface
interface ILogger {
trace(message: string, meta?: Record<string, unknown>): void;
debug(message: string, meta?: Record<string, unknown>): void;
info(message: string, meta?: Record<string, unknown>): void;
warn(message: string, meta?: Record<string, unknown>): void;
error(message: string, error?: Error, meta?: Record<string, unknown>): void;
fatal(message: string, error?: Error, meta?: Record<string, unknown>): void;
child(bindings: Record<string, unknown>): ILogger;
// Optional — extension points
getLogBuffer?(): ILogBuffer | undefined;
onLog?(callback: (record: LogRecord) => void): () => void;
}Log levels
type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';Six levels. trace is the noisiest (function entry/exit, loop iterations); fatal is reserved for unrecoverable failures that terminate the process.
Log record
Every entry produced by the adapter is shaped as:
interface LogRecord {
id: string; // ULID — sortable, compact
timestamp: number; // ms since epoch
level: LogLevel;
message: string;
fields: Record<string, unknown>; // merged meta + child bindings
source: string; // 'rest', 'workflow', 'cli', ...
}All adapters must use generateLogId() from the contract package to produce IDs. It returns a ULID, which is lexicographically sortable and timestamp-based — this means log records can be sorted by ID and you get chronological order for free.
Methods
trace, debug, info, warn
Standard level methods. Each takes a message and optional metadata bag:
logger.info('task started', { taskId: '123' });
logger.warn('slow operation', { durationMs: 5000 });error(message, error?, meta?) and fatal(message, error?, meta?)
Error and fatal take an optional Error object as their second argument, separate from the metadata bag. This lets the adapter extract stack traces and error codes automatically.
try {
await doWork();
} catch (err) {
logger.error('task failed', err instanceof Error ? err : undefined, { taskId: '123' });
throw err;
}Passing an Error is optional — you can call logger.error('something weird') with just a message if you don't have one.
child(bindings)
Returns a new logger instance with persistent context fields merged into every log entry:
const opLogger = logger.child({ operation: 'release', version: '1.0.0' });
opLogger.info('started'); // includes operation + version
opLogger.info('completed'); // sameChild loggers are cheap — create them liberally. Nested children merge: logger.child({ a: 1 }).child({ b: 2 }) produces entries with both fields.
Optional extensions
getLogBuffer()
If the adapter maintains an in-memory ring buffer of recent logs, this returns it:
interface ILogBuffer {
append(record: LogRecord): void;
query(query?: LogQuery): LogRecord[];
subscribe(callback: (record: LogRecord) => void): () => void;
getStats(): { total: number; bufferSize: number; oldestTimestamp: number | null; newestTimestamp: number | null };
}
interface LogQuery {
level?: LogLevel; // minimum level, inclusive
source?: string;
from?: number; // ms
to?: number;
limit?: number;
}The buffer lets Studio, REST endpoints, and CLI commands stream or page through recent logs without touching the persistent store. Not every logger has one — ConsoleLogger doesn't, the Pino adapter with streaming enabled does.
onLog(callback)
Registers a fire-and-forget callback invoked for every log record. Used by extension adapters (ring buffer, SQLite persistence) to hook into the logger without being tightly coupled to it:
const unsubscribe = logger.onLog((record) => {
ringBuffer.append(record);
persistence.write(record).catch(console.error);
});Multiple callbacks can register at once. The logger calls them in priority order (from the extension manifest). Exceptions in callbacks are caught and logged, not propagated.
This is the hook that logRingBuffer and logPersistence extension adapters attach to — see Adapters → Overview → Extension points.
Contract rules
- All methods return
void. No promises. Adapters that persist logs batch internally and flush on a timer or on shutdown. errorandfataltakeErrorseparately from meta. Callers pass the Error as the second argument so the adapter can extract stack traces.child()returns a real logger, not a wrapper. The child implements the fullILoggerinterface.- IDs are ULIDs from
generateLogId(). Don't invent your own — it breaks sortability guarantees. - Adapters are fire-and-forget. Errors inside the logger (e.g. persistence failure) must not propagate to callers.
Writing an ILogger adapter
import type { AdapterManifest, AdapterFactory, ILogger, LogRecord } from '@kb-labs/core-platform';
import { generateLogId } from '@kb-labs/core-platform';
export const manifest: AdapterManifest = {
manifestVersion: '1.0.0',
id: 'console-logger',
name: 'Console Logger',
version: '0.1.0',
type: 'core',
implements: 'ILogger',
capabilities: {},
};
class ConsoleLogger implements ILogger {
constructor(private bindings: Record<string, unknown> = {}, private source = 'app') {}
private write(level: string, message: string, meta?: Record<string, unknown>, error?: Error) {
const record: LogRecord = {
id: generateLogId(),
timestamp: Date.now(),
level: level as any,
message,
fields: { ...this.bindings, ...meta, ...(error && { stack: error.stack }) },
source: this.source,
};
// eslint-disable-next-line no-console
console.log(JSON.stringify(record));
}
trace(m: string, meta?: Record<string, unknown>): void { this.write('trace', m, meta); }
debug(m: string, meta?: Record<string, unknown>): void { this.write('debug', m, meta); }
info(m: string, meta?: Record<string, unknown>): void { this.write('info', m, meta); }
warn(m: string, meta?: Record<string, unknown>): void { this.write('warn', m, meta); }
error(m: string, err?: Error, meta?: Record<string, unknown>): void { this.write('error', m, meta, err); }
fatal(m: string, err?: Error, meta?: Record<string, unknown>): void { this.write('fatal', m, meta, err); }
child(bindings: Record<string, unknown>): ILogger {
return new ConsoleLogger({ ...this.bindings, ...bindings }, this.source);
}
}
export const createAdapter: AdapterFactory<unknown, {}, ILogger> = () => new ConsoleLogger();Real adapters (Pino) use a fast JSON serializer, support log rotation, and emit via onLog for extension hookups.
Built-in adapters implementing ILogger
| Package | Notes |
|---|---|
@kb-labs/adapters-pino | Pino. Streaming support, pretty printing in dev, configurable log levels, ring buffer integration. |
Extension adapters that hook into logger.onLog:
| Package | Notes |
|---|---|
@kb-labs/adapters-log-ringbuffer | Keeps the last N logs in memory for Studio/REST access. Priority 10. |
@kb-labs/adapters-log-sqlite | Persists logs to SQLite with retention policies. Priority 5. Depends on db. |
What to read next
- SDK → Hooks →
useLogger— plugin consumer. - Adapters → Overview → Extension points — how
onLogextension adapters work.