@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, 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

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');