@cleverbrush/log

Enterprise structured logging for TypeScript — Serilog-style message templates, CLEF format, batching sinks, correlation tracking.

$npm install @cleverbrush/log

Optional peer dependencies: @cleverbrush/di for DI integration, @cleverbrush/server for HTTP middleware.

💡 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 } from '@cleverbrush/log';

const logger = createLogger({
    minimumLevel: 'information',
    sinks: [consoleSink({ theme: 'dark' })],
    enrichers: ['hostname', 'processId'],
});

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

import { createLogger, consoleSink, SeqSink, FileSink } from '@cleverbrush/log';

const logger = createLogger({
    minimumLevel: 'debug',
    sinks: [
        consoleSink({ theme: 'dark', minimumLevel: 'information' }),
        new SeqSink({ serverUrl: 'http://localhost:5341' }),
        new FileSink({ path: './logs/app.clef', rotationPolicy: 'size', maxFileSize: 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, ClickStack, ClickHouse, etc.

import { s } from '@cleverbrush/schema';
import { createLogger, consoleSink } from '@cleverbrush/log';

// Define once — compile-time checked parameter types
const TodoCreated = s.parseString('Todo #{TodoId} "{Title}" created by {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 } from '@cleverbrush/log';

const logger = createLogger({
    minimumLevel: 'information',
    sinks: [consoleSink()],
    enrichers: ['correlationId'],
});

// 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 { configureLogging, ILogger, consoleSink } from '@cleverbrush/log';

const services = new ServiceCollection();
configureLogging(services, logger);

const provider = services.buildServiceProvider();
const logger = provider.getService(ILogger);
logger.info('Resolved from DI');