The logger system is the diagnostic side of the engine. It lets you emit messages at log levels, register custom sinks, buffer per-context logs, and in Node.js write logs to files.
Use it when you need operational diagnostics rather than business-level audit data.
The main logging types are:
Logger for global logger registration, log levels, and dispatchILogger for custom logger implementationsAbstractLogger as a base class for custom loggersConsoleLogger for direct console outputMemoryLogger for storing structured log events in memoryNoopLogger for explicitly discarding log outputContextLogger for buffered per-context loggingSupported log levels are:
tracedebuginfowarnerrorfatalThe global log level controls which messages are emitted.
import { Logger, Workspace } from '@samatawy/rules';
Logger.setLogLevel('warn');
const workspace = new Workspace({ strict_inputs: false, strict_outputs: false });
workspace.addRule('IF total > 100 THEN approved = true');
const ctx = workspace.loadContext({ total: 120 });
workspace.process(ctx);
ctx.logger().flush();
Logger.flush();
Practical guidance:
warn or error in production to keep output manageableinfo, debug, or trace while investigating problemsThe context logger and the audit trail solve different problems.
ctx.getLog() describes which rules ran and what effect they hadctx.getExceptions() describes rule exceptionsctx.logger() is for diagnostic messages produced during processingctx.logger() is typically a ContextLogger. It buffers events during the run and forwards them to registered loggers when flush() is called.
To connect the engine to your own logging system such as Pino, Winston, or Sentry, implement ILogger and register it with Logger.register().
import { AbstractLogger, Logger, type LogLevel } from '@samatawy/rules';
class ArrayLogger extends AbstractLogger {
public events: Array<{ level: LogLevel; msg: string; args: unknown[] }> = [];
private push(level: LogLevel, msg: string, ...args: unknown[]): void {
if (this.canLog(level)) {
this.events.push({ level, msg, args });
}
}
public trace(msg: string, ...args: unknown[]): void { this.push('trace', msg, ...args); }
public debug(msg: string, ...args: unknown[]): void { this.push('debug', msg, ...args); }
public info(msg: string, ...args: unknown[]): void { this.push('info', msg, ...args); }
public warn(msg: string, ...args: unknown[]): void { this.push('warn', msg, ...args); }
public error(msg: string, ...args: unknown[]): void { this.push('error', msg, ...args); }
public fatal(msg: string, ...args: unknown[]): void { this.push('fatal', msg, ...args); }
public log(level: LogLevel, msg: string, ...args: unknown[]): void { this.push(level, msg, ...args); }
public flush(): void {
for (const event of this.events) {
console.log(`[${event.level}]`, event.msg, ...event.args);
}
this.events = [];
}
}
const appLogger = new ArrayLogger();
Logger.setLogLevel('info');
Logger.register('app', appLogger);
Logger.info('Rules engine started');
Logger.warn('Rule evaluation took longer than expected', { workspace: 'pricing' });
Logger.flush();
Remove a logger with Logger.unregister('app') or by passing the logger instance.
Built-in logger implementations currently include:
ConsoleLogger for direct console outputMemoryLogger for tests and in-memory diagnostics; calling flush() clears captured eventsNoopLogger for intentionally suppressing log outputContextLogger for buffering log events per processing contextUse withLogger() when you want a particular call tree to emit through a different logger without permanently changing global logger setup.
import { Logger, withLogger, type ILogger } from '@samatawy/rules';
declare const requestLogger: ILogger;
declare function runRulePass(input: unknown): unknown;
const runWithRequestLogger = withLogger(requestLogger, runRulePass);
Logger.setLogLevel('debug');
runWithRequestLogger({ total: 120 });
The override applies only while the wrapped function executes.
If you run the engine in Node.js, you can write logs to disk with FileLoggerFactory.
This feature is intended for the default Node package entry. It should not be used from the browser build because browsers do not provide a file system.
import fs from 'node:fs';
import { FileLoggerFactory, Logger } from '@samatawy/rules';
const fileLogger = FileLoggerFactory.create({
directory: './logs',
baseName: 'rules',
level: 'info',
rotation: { kind: 'size', maxBytes: 1_000_000 },
}, fs);
Logger.register('file', fileLogger);
Logger.info('Rules engine started');
Logger.flush();
Supported rotation modes are:
run for one file per logger instancesize for a new file once the current file would exceed maxBytesinterval for a new file every everySeconds secondsboundary for rollover on hour, day, week, or month, with optional utc: trueGenerated file names are human-readable and safe for common file systems, using a timestamp such as rules.2026-06-05_09-58-49.194.log.
There are two supported creation paths:
FileLoggerFactory.create(options, fs) when you already have Node's fs module and want synchronous deterministic setupawait FileLoggerFactory.createAsync(options) when you want the factory to load the file system module for youBoth creation paths require a Node.js runtime and throw if used outside Node.
If you want custom file output, provide a formatter through the formatter option. LoggedEventFormatter is the built-in helper for templated event formatting.
import fs from 'node:fs';
import { FileLoggerFactory, LoggedEventFormatter, Logger } from '@samatawy/rules';
const fileLogger = FileLoggerFactory.create({
directory: './logs',
baseName: 'rules',
formatter: LoggedEventFormatter.using('{timestamp} [{level}] {message}[? :: {0}]'),
rotation: { kind: 'boundary', unit: 'day' },
}, fs);
Logger.register('file', fileLogger);
LoggedEventFormatter formats a LoggedEvent, so it works with the fixed event fields plus positional logger arguments.
These placeholders are always supported:
{timestamp} for the event timestamp, formatted as an ISO string{level} for the log level in uppercase{message} for the main log messageYou can also reference logger arguments by zero-based index:
{0} for the first extra argument{1} for the second extra argument{2} for the third extra argumentand so on.
Two wildcard placeholders are also supported:
{args} or {*} for all extra arguments not already referenced by numbered placeholders in the templateWildcard placeholders render the remaining arguments as a bracketed comma-separated list.
For example, this call:
Logger.info('Rule evaluation finished', { workspace: 'pricing' }, 42);
can be formatted with {message}, {0}, and {1}. If you also use {args} or {*}, only the extra arguments not already claimed by numbered placeholders are included there.
LoggedEventFormatter also supports optional blocks in the form [? ... ].
An optional block is included only if every placeholder inside that block is available for that event.
For example:
const formatter = LoggedEventFormatter.using(
'{timestamp} [{level}] {message}[? :: {0}][? :: code={1}]'
);
{0} and {1} exist, both optional blocks are included{0} exists, the first block is included and the second is removed{args} or {*} is used inside an optional block, that block is included only when at least one remaining argument existsExample template:
const formatter = LoggedEventFormatter.using(
'{timestamp} [{level}] {message}[? :: {0}]'
);
Example event call:
Logger.warn('Rule evaluation took longer than expected', { workspace: 'pricing' });
Example output:
2026-06-05T10:15:30.000Z [WARN] Rule evaluation took longer than expected :: {"workspace":"pricing"}
Another template:
const formatter = LoggedEventFormatter.using(
'{timestamp} [{level}] {message}[? | request={0}][? | attempt={1}]'
);
Possible output when both arguments are present:
2026-06-05T10:15:30.000Z [INFO] Retrying rule pass | request=pricing | attempt=2
Possible output when only the first argument is present:
2026-06-05T10:15:30.000Z [INFO] Retrying rule pass | request=pricing
Template using remaining args:
const formatter = LoggedEventFormatter.using(
'{timestamp} [{level}] {message}[? | primary={0}][? | rest={args}]'
);
Example event call:
Logger.warn('Rule evaluation took longer than expected', { workspace: 'pricing' }, 2, true);
Example output:
2026-06-05T10:15:30.000Z [WARN] Rule evaluation took longer than expected | primary={"workspace":"pricing"} | rest=[2, true]
{*} behaves the same way as {args}.
true or falseDate values are rendered as ISO stringsThe asynchronous creation path looks like this:
import { FileLoggerFactory, Logger } from '@samatawy/rules';
const fileLogger = await FileLoggerFactory.createAsync({
directory: './logs',
baseName: 'rules',
rotation: { kind: 'interval', everySeconds: 300 },
});
Logger.register('file', fileLogger);
maxFiles is reserved for retention control, but it is not enforced yet.