@nowarajs/logger
    Preparing search index...

    @nowarajs/logger

    🎯 NowaraJS Logger

    nowarajs-logger-wall

    Logging in Bun often means choosing between "fast but dumb" or "smart but blocking". I built NowaraJS Logger because I wanted both: a type-safe, sink-based system that never blocks your main thread.

    The goal is simple: Stop your logs from slowing down your app.

    Most loggers either block on every write or lose type safety when you need structured logging. This package runs everything in a worker thread, batches automatically, and still gives you full TypeScript inference on what you log.

    • Zero Blocking: Every log goes through a worker thread – your main loop stays fast.
    • 🔒 Type-Safe: TypeScript infers the shape of your logs. No more any everywhere.
    • 🎯 Sink Pattern: Route logs to console, file, database, or your own custom destination.
    • 🔄 Smart Batching: Logs are grouped automatically for better I/O performance.
    • 🔔 Event-Driven: Listen to flush, close, and error events when you need them.
    bun add @nowarajs/logger
    

    You'll also need:

    bun add @nowarajs/error @nowarajs/typed-event-emitter
    

    Create a logger, attach a sink, and start logging:

    import { Logger } from '@nowarajs/logger';
    import { ConsoleLoggerSink } from '@nowarajs/logger/sinks';

    // Create a logger and register a console sink
    const logger = new Logger().registerSink('console', ConsoleLoggerSink);

    // Log messages (always pass an object)
    logger.info({ message: 'Application started' });
    logger.warn({ message: 'This is a warning' });
    logger.error({ message: 'An error occurred', code: 500 });
    logger.debug({ action: 'debug_info', data: { foo: 'bar' } });
    logger.log({ event: 'generic_log' });

    // Close the logger when done
    await logger.close();

    Need logs going to different places? Register as many sinks as you want:

    import { Logger } from '@nowarajs/logger';
    import { ConsoleLoggerSink, FileLoggerSink } from '@nowarajs/logger/sinks';

    // Register multiple sinks
    const logger = new Logger()
    .registerSink('console', ConsoleLoggerSink)
    .registerSink('file', FileLoggerSink, './app.log');

    // Log to all sinks
    logger.info({ message: 'This goes to console and file' });

    // Log to specific sinks only
    logger.error({ message: 'Only in file' }, ['file']);
    logger.warn({ message: 'Only in console' }, ['console']);

    await logger.close();

    Have a weird logging requirement? Write your own sink:

    import type { LoggerSink, LogLevels } from '@nowarajs/logger/types';

    // Create a custom sink
    class DatabaseSink implements LoggerSink {
    public async log(level: LogLevels, timestamp: number, object: unknown): Promise<void> {
    // Your custom logging logic
    await saveToDatabase({ level, timestamp, object });
    }
    }

    const logger = new Logger().registerSink('database', DatabaseSink);

    logger.info({ event: 'user_created', userId: 42 });
    await logger.close();

    This is where it gets interesting. When you define typed sinks, TypeScript knows exactly what shape your logs need. No more guessing, no more runtime surprises.

    import type { LoggerSink, LogLevels } from '@nowarajs/logger/types';

    // Define your log object type
    interface UserLog {
    userId: number;
    action: string;
    timestamp?: Date;
    }

    // Create a typed sink
    class UserLogSink implements LoggerSink<UserLog> {
    public async log(level: LogLevels, timestamp: number, object: UserLog): Promise<void> {
    console.log(`User ${object.userId} performed: ${object.action}`);
    }
    }

    const logger = new Logger().registerSink('userLog', UserLogSink);

    // ✅ TypeScript requires the correct shape
    logger.info({
    userId: 123,
    action: 'login'
    });

    // ❌ TypeScript error: Missing required property 'action'
    logger.info({
    userId: 123
    // Error: Property 'action' is missing
    });

    When logging to multiple sinks at once, TypeScript creates an intersection of all types. You need to satisfy all of them:

    interface UserLog {
    userId: number;
    action: string;
    }

    interface ApiLog {
    endpoint: string;
    method: string;
    statusCode: number;
    }

    class UserLogSink implements LoggerSink<UserLog> {
    public async log(level: LogLevels, timestamp: number, object: UserLog): Promise<void> {
    await saveUser(object);
    }
    }

    class ApiLogSink implements LoggerSink<ApiLog> {
    public async log(level: LogLevels, timestamp: number, object: ApiLog): Promise<void> {
    await saveApi(object);
    }
    }

    const logger = new Logger().registerSink('user', UserLogSink).registerSink('api', ApiLogSink);

    // ✅ When using both sinks, you need BOTH types combined
    logger.info(
    {
    userId: 123,
    action: 'api_call',
    endpoint: '/users',
    method: 'POST',
    statusCode: 201
    },
    ['user', 'api']
    ); // Logs to both sinks

    // ✅ When using only one sink, only that type is required
    logger.warn(
    {
    userId: 456,
    action: 'failed_attempt'
    },
    ['user']
    ); // Only UserLog type required

    // ❌ TypeScript error: Missing api properties
    logger.error(
    {
    userId: 789,
    action: 'error'
    },
    ['user', 'api']
    );

    When you mix typed sinks with untyped ones (like ConsoleLoggerSink which accepts unknown), things stay flexible:

    interface DatabaseLog {
    query: string;
    duration: number;
    }

    class DatabaseLogSink implements LoggerSink<DatabaseLog> {
    public async log(level: LogLevels, timestamp: number, object: DatabaseLog): Promise<void> {
    await logToDatabase(object);
    }
    }

    const logger = new Logger()
    .registerSink('database', DatabaseLogSink)
    .registerSink('console', ConsoleLoggerSink); // Accepts unknown

    // ✅ This works - intersection with unknown allows extra properties
    logger.info(
    {
    query: 'SELECT * FROM users',
    duration: 123,
    customData: 'anything goes'
    },
    ['database', 'console']
    );

    Things break. When they do, you'll want to know:

    const logger = new Logger().registerSink('console', ConsoleLoggerSink);

    // Listen for errors
    logger.addListener('sinkError', (error) => {
    console.error('Logger error:', error.message);
    });

    logger.addListener('registerSinkError', (error) => {
    console.error('Failed to register sink:', error.message);
    });

    logger.info({ message: 'Safe to log' });
    await logger.close();

    When you need to make sure everything is written before shutting down:

    const logger = new Logger().registerSink('console', ConsoleLoggerSink);

    logger.info({ message: 'First message' });
    logger.info({ message: 'Second message' });

    // Wait for all pending logs to be processed
    await logger.flush();

    // Close the logger and release resources (internally calls flush)
    await logger.close();

    Fine-tune the batching and queue behavior:

    const logger = new Logger({
    maxPendingLogs: 5000, // Max queued logs (default: 10,000)
    batchSize: 50, // Logs per batch (default: 50)
    batchTimeout: 100, // Ms before flushing batch (default: 0.1)
    maxMessagesInFlight: 100, // Max batches being processed (default: 100)
    autoEnd: true, // Auto-close on process exit (default: true)
    flushOnBeforeExit: true // Flush before exit (default: true)
    });

    Full docs: nowarajs.github.io/logger

    MIT – Use it however you want.