@cleverbrush/log
Enterprise structured logging for TypeScript — Serilog-style message templates, CLEF format, batching sinks, correlation tracking.
💡 Why @cleverbrush/log?
The Problem
Most Node.js loggers produce unstructured text that is painful to query. console.log and basic loggers lose context across async boundaries, and correlating requests through a distributed system requires custom plumbing.
The Solution
@cleverbrush/logbrings .NET's Serilog model to TypeScript: message templates that preserve named properties, automatic CLEF formatting for machine-readable output, batching sinks with retry and circuit breaking, and ambient correlation IDs via AsyncLocalStorage.
🚀 Quick Start
import { createLogger, consoleSink, hostnameEnricher, processIdEnricher } from '@cleverbrush/log';
const logger = createLogger({
minimumLevel: 'information',
sinks: [consoleSink({ theme: 'dark' })],
enrichers: [hostnameEnricher(), processIdEnricher()],
});
logger.info('Server started on port {Port}', { Port: 3000 });
logger.error(new Error('oops'), 'Request failed for {UserId}', { UserId: 42 });
// Graceful shutdown
await logger.dispose();📝 Message Templates
Properties in {braces} are captured as structured data and rendered into the message. Use @ to destructure objects.
// Named properties — queryable in Seq/ClickHouse
logger.info('Order {OrderId} placed by {UserId}', { OrderId: 123, UserId: 'u-42' });
// Destructure with @
logger.info('Config loaded: {@Config}', { Config: { port: 3000, env: 'prod' } });🔌 Sinks
- consoleSink — Pretty or JSON/CLEF output with configurable themes
- FileSink — CLEF files with size/time rotation
- SeqSink — Ships to Seq with dynamic level control
- ClickHouseSink — Batch insert to ClickHouse (separate entrypoint)
- createSink — Build a custom sink from a simple function
import { createLogger, consoleSink, seqSink, fileSink } from '@cleverbrush/log';
const logger = createLogger({
minimumLevel: 'debug',
sinks: [
consoleSink({ theme: 'dark', minimumLevel: 'information' }),
seqSink({ serverUrl: 'http://localhost:5341' }),
fileSink({ path: './logs/app.clef', rotation: { strategy: 'size', maxBytes: 10_000_000 } }),
],
});🔷 Typed Templates
Pass a ParseStringSchemaBuilder (from @cleverbrush/schema) directly to any log method. TypeScript enforces the parameter types at the call site, and the logger uses the raw {Property} pattern as messageTemplate so all events of the same shape are grouped in Seq, SigNoz, ClickHouse, etc.
import { parseString, object, string, number } from '@cleverbrush/schema';
import { createLogger, consoleSink } from '@cleverbrush/log';
// Define once — compile-time checked parameter types
const TodoCreated = parseString(
object({ TodoId: number(), Title: string(), UserId: string() }),
$t => $t`Todo #${t => t.TodoId} "${t => t.Title}" created by ${t => t.UserId}`
);
const logger = createLogger({ sinks: [consoleSink()] });
// TypeScript enforces { TodoId, Title, UserId }
logger.info(TodoCreated, { TodoId: 1, Title: 'Buy milk', UserId: 'u-42' });
// messageTemplate → 'Todo #{TodoId} "{Title}" created by {UserId}'
// renderedMessage → 'Todo #1 "Buy milk" created by u-42'🔗 Correlation & Middleware
import { useLogging, createLogger, consoleSink, correlationIdEnricher } from '@cleverbrush/log';
const logger = createLogger({
minimumLevel: 'information',
sinks: [consoleSink()],
enrichers: [correlationIdEnricher()],
});
// Returns [correlationIdMiddleware, requestLoggingMiddleware]
const [correlationId, requestLogging] = useLogging(logger, {
excludePaths: ['/health'],
// Set to false when @cleverbrush/otel's tracingMiddleware already
// sets a traceparent header — avoids a redundant second ID header
correlationResponseHeader: false,
});
// Add to your @cleverbrush/server pipeline
// Every request gets a unique correlation ID, logged on completion🧩 DI Integration
import { ServiceCollection } from '@cleverbrush/di';
import { createLogger, configureLogging, ILogger, consoleSink } from '@cleverbrush/log';
const logger = createLogger({ sinks: [consoleSink()] });
const services = new ServiceCollection();
configureLogging(services, logger);
const provider = services.buildServiceProvider();
const resolvedLogger = provider.getService(ILogger);
resolvedLogger.info('Resolved from DI');