📦
Bunty

Logging

Bunty includes a built-in structured logger with colored terminal output, timestamps, scopes, and configurable log levels. No external dependencies required.

Quick Start

Basic Usage

import { Logger } from '@bunty/common';

// Use the default logger
Logger.info('Application started');
Logger.success('Database connected');
Logger.warn('Cache miss detected');
Logger.error('Failed to fetch data');
Logger.debug('Request payload:', { userId: 123 });

Scoped Logger

Create loggers for specific services or modules:

import { Logger } from '@bunty/common';

// Create a scoped logger
const logger = new Logger('UserService');

logger.info('Fetching user data');
logger.success('User created:', user);
logger.warn('User not found');
logger.error('Database error:', error);

Log Levels

Bunty supports six log levels, each with distinct colors:

LevelMethodColorUse Case
INFO.info()CyanInformational messages
SUCCESS.success()GreenSuccessful operations
WARN.warn()YellowWarning messages
DEBUG.debug()GrayDebug information
ERROR.error()RedError messages
LOG.log()WhiteGeneral messages

Terminal Output Example

Here’s what the logger looks like in your terminal:

[2025/10/30 19:53:14] [INFO] [PaymentService] Processing payment request
[2025/10/30 19:53:15] [DEBUG] [PaymentService] Order data: { id: 12345, amount: 99.99, currency: “USD” }
[2025/10/30 19:53:16] [SUCCESS] [PaymentService] Payment processed successfully { chargeId: “ch_1234567890” }
[2025/10/30 19:53:17] [WARN] [CacheService] Cache miss detected for key: user:123
[2025/10/30 19:53:18] [ERROR] [DatabaseConn] Connection timeout after 5000ms
[2025/10/30 19:53:19] [LOG] [App] Application initialized

Visual breakdown:

  • 🕐 Timestamp (gray) - [2025/10/30 19:53:14]
  • 🏷️ Level (colored) - [INFO], [SUCCESS], [ERROR], etc.
  • 📦 Scope (cyan) - [PaymentService], [CacheService], etc.
  • 💬 Message (colored) - The actual log message with data

Code Examples

Code Examples

const logger = new Logger('PaymentService');

// Info - general information
logger.info('Processing payment for order', orderId);

// Success - successful operations
logger.success('Payment processed successfully', { 
    amount: 99.99, 
    orderId: 'ORD-123' 
});

// Warn - potential issues
logger.warn('Payment gateway response time slow', { 
    responseTime: 3500 
});

// Debug - detailed debugging info
logger.debug('Payment request payload:', {
    cardLast4: '4242',
    amount: 99.99,
    currency: 'USD'
});

// Error - failures and exceptions
logger.error('Payment failed:', new Error('Insufficient funds'));

// Log - general purpose
logger.log('Payment webhook received');

Output in terminal:

[2025/10/30 19:53:14] [INFO] [PaymentService] Processing payment for order 12345
[2025/10/30 19:53:15] [SUCCESS] [PaymentService] Payment processed successfully { amount: 99.99, orderId: “ORD-123” }
[2025/10/30 19:53:16] [WARN] [PaymentService] Payment gateway response time slow { responseTime: 3500 }
[2025/10/30 19:53:17] [DEBUG] [PaymentService] Payment request payload: { cardLast4: “4242”, amount: 99.99, currency: “USD” }
[2025/10/30 19:53:18] [ERROR] [PaymentService] Payment failed: Error: Insufficient funds
at processPayment (C:\app\services\payment.ts:42:11)
at async POST (C:\app\routes\api\payments.ts:15:5)
[2025/10/30 19:53:19] [LOG] [PaymentService] Payment webhook received

Multiple Services Example

Different scopes in action:

const dbLogger = new Logger('Database');
const cacheLogger = new Logger('Cache');
const apiLogger = new Logger('API');
const workerLogger = new Logger('SyncWorker');

dbLogger.success('Connected to PostgreSQL');
cacheLogger.success('Connected to Redis');
apiLogger.info('Server listening on port 3000');
workerLogger.info('Background sync started');

Terminal output:

[2025/10/30 19:53:14] [SUCCESS] [Database] Connected to PostgreSQL
[2025/10/30 19:53:14] [SUCCESS] [Cache] Connected to Redis
[2025/10/30 19:53:15] [INFO] [API] Server listening on port 3000
[2025/10/30 19:53:16] [INFO] [SyncWorker] Background sync started

Output Format

Logs are formatted with timestamp, level, scope, and message:

Format Structure

[TIMESTAMP       ] [LEVEL  ] [SCOPE        ] MESSAGE AND DATA
└─ Gray             └─ Color   └─ Cyan         └─ Colored with formatting

Real-World Example

const logger = new Logger('UserService');

logger.info('Fetching user data');
logger.success('User created', { id: 123, email: 'user@example.com' });
logger.warn('User not found');
logger.error('Database error:', new Error('Connection timeout'));

Terminal output:

[2025/10/30 19:53:14] [INFO] [UserService] Fetching user data
[2025/10/30 19:53:15] [SUCCESS] [UserService] User created { id: 123, email: “user@example.com” }
[2025/10/30 19:53:16] [WARN] [UserService] User not found
[2025/10/30 19:53:17] [ERROR] [UserService] Database error: Error: Connection timeout
at connectDatabase (C:\app\db\connection.ts:28:9)
at async UserService.fetchUser (C:\app\services\user.ts:15:5)

Component Breakdown

ComponentWidthColorExample
Timestamp19 charsGray[2025/10/30 19:53:14]
LevelVariable + paddingVaries[INFO], [SUCCESS], [ERROR]
ScopeVariable + paddingCyan[UserService], [Database]
MessageVariableVariesColored based on log level

Creating Loggers

Scoped Logger

import { Logger } from '@bunty/common';

const logger = new Logger('OrderService');

logger.info('Order service initialized');
logger.success('Order created:', order);

Default Logger

Use static methods for quick logging:

import { Logger } from '@bunty/common';

Logger.info('Using default logger');
Logger.success('Operation completed');
Logger.error('Something went wrong');

Multiple Scopes

Create different loggers for different parts of your application:

const dbLogger = new Logger('Database');
const cacheLogger = new Logger('Cache');
const apiLogger = new Logger('API');

dbLogger.info('Connected to PostgreSQL');
cacheLogger.info('Connected to Redis');
apiLogger.info('Server listening on port 3000');

Log Level Control

Control which log levels are enabled using bitwise flags:

LogFlags Enum

import { Logger, LogFlags } from '@bunty/common';

enum LogFlags {
    None = 0,           // No logging
    Info = 1 << 0,      // Info messages
    Success = 1 << 1,   // Success messages
    Warn = 1 << 2,      // Warning messages
    Debug = 1 << 3,     // Debug messages
    Error = 1 << 4,     // Error messages
    Log = 1 << 5,       // General log messages
    All = Info | Success | Warn | Debug | Error | Log
}

Enable/Disable Levels

const logger = new Logger('MyService');

// Only log errors and warnings
logger.setFlags(LogFlags.Error | LogFlags.Warn);
logger.info('This will NOT be logged');
logger.error('This WILL be logged');

// Enable all levels
logger.setFlags(LogFlags.All);

// Disable all logging
logger.setFlags(LogFlags.None);

Production vs Development

import { env, Logger, LogFlags } from '@bunty/common';

const logger = new Logger('App');

// Production: only errors and warnings
if (env('NODE_ENV') === 'production') {
    logger.setFlags(LogFlags.Error | LogFlags.Warn);
}
// Development: all logs including debug
else {
    logger.setFlags(LogFlags.All);
}

Add/Remove Flags

const logger = new Logger('Service');

// Add debug flag
logger.addFlags(LogFlags.Debug);

// Remove info flag
logger.removeFlags(LogFlags.Info);

// Check current flags
const flags = logger.getFlags();

Logging Complex Data

The logger automatically formats different data types:

Objects

logger.info('User data:', {
    id: 123,
    email: 'user@example.com',
    roles: ['admin', 'user'],
    meta: { lastLogin: new Date() }
});

Terminal output:

[2025/10/30 19:53:14] [INFO] [App] User data: {
id: 123,
email: “user@example.com”,
roles: [ “admin”, “user” ],
meta: {
lastLogin: 2025-10-30T19:53:14.000Z
}
}

Errors

try {
    await riskyOperation();
} catch (error) {
    logger.error('Operation failed:', error);
}

Terminal output (stack trace in red):

[2025/10/30 19:53:14] [ERROR] [App] Operation failed: Error: Connection timeout
at riskyOperation (C:\app\service.ts:42:11)
at async processRequest (C:\app\handlers\request.ts:28:5)
at async Server.handleRequest (C:\app\server.ts:15:3)

Primitives

logger.info('Order count:', 42);
logger.info('Is active:', true);
logger.info('Status:', null);
logger.info('User name:', 'John Doe');

Terminal output:

[2025/10/30 19:53:14] [INFO] [App] Order count: 42
[2025/10/30 19:53:15] [INFO] [App] Is active: true
[2025/10/30 19:53:16] [INFO] [App] Status: null
[2025/10/30 19:53:17] [INFO] [App] User name: John Doe

Mixed Types

logger.debug(
    'Request processed',
    { method: 'POST', path: '/api/users' },
    'in',
    123,
    'ms'
);

Terminal output:

[2025/10/30 19:53:14] [DEBUG] [App] Request processed { method: “POST”, path: “/api/users” } in 123 ms

Use in Services

Injectable Logger

import { Injectable, Logger } from '@bunty/common';

@Injectable()
export class UserService {
    private logger = new Logger('UserService');

    async createUser(data: CreateUserDto) {
        this.logger.info('Creating user:', data.email);

        try {
            const user = await this.db.insert(usersTable, data);
            this.logger.success('User created:', { id: user.id });
            return user;
        } catch (error) {
            this.logger.error('Failed to create user:', error);
            throw error;
        }
    }

    async deleteUser(id: number) {
        this.logger.warn('Deleting user:', id);
        await this.db.delete(usersTable, id);
        this.logger.success('User deleted');
    }
}

Constructor Injection Pattern

import { Injectable, Logger } from '@bunty/common';

@Injectable()
export class OrderService {
    private readonly logger: Logger;

    constructor() {
        this.logger = new Logger('OrderService');
    }

    async processOrder(orderId: number) {
        this.logger.info('Processing order:', orderId);

        const order = await this.fetchOrder(orderId);
        this.logger.debug('Order fetched:', order);

        await this.validateOrder(order);
        this.logger.success('Order validated');

        await this.chargeCustomer(order);
        this.logger.success('Customer charged');

        return order;
    }
}

HTTP Request Logging

Log Incoming Requests

import { createApp, Logger } from '@bunty/common';

const app = createApp();
const logger = new Logger('HTTP');

app.use((req, res, next) => {
    const start = Date.now();
    
    logger.info(`${req.method} ${req.path}`);
    
    res.on('finish', () => {
        const duration = Date.now() - start;
        logger.success(
            `${req.method} ${req.path}`,
            res.statusCode,
            `${duration}ms`
        );
    });
    
    next();
});

Terminal output:

[2025/10/30 19:53:14] [INFO] [HTTP] GET /api/users
[2025/10/30 19:53:15] [SUCCESS] [HTTP] GET /api/users 200 42ms
[2025/10/30 19:53:16] [INFO] [HTTP] POST /api/orders
[2025/10/30 19:53:17] [SUCCESS] [HTTP] POST /api/orders 201 156ms
[2025/10/30 19:53:18] [INFO] [HTTP] DELETE /api/products/123
[2025/10/30 19:53:19] [SUCCESS] [HTTP] DELETE /api/products/123 204 23ms

Log Errors

app.use((error, req, res, next) => {
    const logger = new Logger('ErrorHandler');
    
    logger.error('Request failed:', {
        method: req.method,
        path: req.path,
        error: error.message,
        stack: error.stack
    });
    
    res.status(500).json({ error: 'Internal server error' });
});

Terminal output:

[2025/10/30 19:53:14] [ERROR] [ErrorHandler] Request failed: {
method: “POST”,
path: “/api/users”,
error: “Validation failed”,
stack: “Error: Validation failed\n at validateUser (C:\app\validators\user.ts:15:11)“
}

Worker Logging

import { createWorker, Injectable, Logger } from '@bunty/common';

@Injectable()
class SyncWorker {
    private logger = new Logger('SyncWorker');

    async onInit() {
        this.logger.info('Worker initializing...');
    }

    async run() {
        this.logger.info('Starting sync...');

        try {
            const records = await this.fetchRecords();
            this.logger.debug('Fetched records:', records.length);

            await this.processRecords(records);
            this.logger.success('Sync completed');
        } catch (error) {
            this.logger.error('Sync failed:', error);
            throw error;
        }
    }

    async onShutdown() {
        this.logger.warn('Worker shutting down...');
    }
}

createWorker({
    name: 'sync-worker',
    interval: '5m',
    tasks: [SyncWorker]
}).start();

Terminal output:

[2025/10/30 19:53:00] [INFO] [SyncWorker] Worker initializing…
[2025/10/30 19:53:05] [INFO] [SyncWorker] Starting sync…
[2025/10/30 19:53:06] [DEBUG] [SyncWorker] Fetched records: 1523
[2025/10/30 19:53:12] [SUCCESS] [SyncWorker] Sync completed
[2025/10/30 19:58:05] [INFO] [SyncWorker] Starting sync…
[2025/10/30 19:58:06] [DEBUG] [SyncWorker] Fetched records: 1547
[2025/10/30 19:58:13] [SUCCESS] [SyncWorker] Sync completed
[2025/10/30 20:03:05] [INFO] [SyncWorker] Starting sync…
[2025/10/30 20:03:06] [DEBUG] [SyncWorker] Fetched records: 1532
[2025/10/30 20:03:07] [ERROR] [SyncWorker] Sync failed: Error: Network timeout
at fetchRecords (C:\app\workers\sync.ts:23:11)
at async SyncWorker.run (C:\app\workers\sync.ts:15:5)

Database Query Logging

import { Injectable, Logger } from '@bunty/common';

@Injectable()
export class ProductService {
    private logger = new Logger('ProductService');

    async findProducts(filters: ProductFilters) {
        this.logger.debug('Query filters:', filters);

        const query = this.db
            .createQueryBuilder(productsTable)
            .where('category', '=', filters.category);

        this.logger.debug('Generated SQL:', query.toSQL());

        const products = await query.findMany();

        this.logger.info('Found products:', products.length);

        return products;
    }
}

Custom Log Methods

Extend Logger

class CustomLogger extends Logger {
    // Custom log level for API calls
    api(method: string, url: string, status: number) {
        this.writeLine(
            'API',
            Color.BrightMagenta,
            method,
            url,
            status
        );
    }

    // Custom log level for metrics
    metric(name: string, value: number, unit: string) {
        this.writeLine(
            'METRIC',
            Color.BrightYellow,
            name,
            value,
            unit
        );
    }
}

const logger = new CustomLogger('App');

logger.api('GET', '/api/users', 200);
logger.metric('response_time', 42, 'ms');

Colors

The logger provides access to ANSI color codes:

Standard Colors

import { Color, Logger } from '@bunty/common';

const logger = new Logger('App');

logger.write(Color.Red, 'This is red', Color.Reset, '\n');
logger.write(Color.Green, 'This is green', Color.Reset, '\n');
logger.write(Color.Yellow, 'This is yellow', Color.Reset, '\n');
logger.write(Color.Cyan, 'This is cyan', Color.Reset, '\n');

Bright Colors

logger.write(Color.BrightRed, 'Bright red', Color.Reset, '\n');
logger.write(Color.BrightGreen, 'Bright green', Color.Reset, '\n');
logger.write(Color.BrightYellow, 'Bright yellow', Color.Reset, '\n');

256 Color Palette

import { Color256, Logger } from '@bunty/common';

const logger = new Logger('App');

logger.write(Color256[196], 'Custom color!', Color.Reset, '\n');
logger.write(Color256[93], 'Another color!', Color.Reset, '\n');

Performance Considerations

Conditional Logging

const logger = new Logger('Service');

// Debug logs are skipped if debug flag is disabled
logger.debug('Expensive operation:', computeExpensiveData());

// Better: only compute if debug is enabled
if (logger.getFlags() & LogFlags.Debug) {
    logger.debug('Expensive operation:', computeExpensiveData());
}

Lazy Evaluation

class SmartLogger extends Logger {
    debugLazy(message: string, dataFn: () => any) {
        if (this.getFlags() & LogFlags.Debug) {
            this.debug(message, dataFn());
        }
    }
}

const logger = new SmartLogger('Service');

// Data function only runs if debug is enabled
logger.debugLazy('Complex data:', () => {
    return expensiveComputation();
});

Testing with Logger

Mock Logger

class MockLogger extends Logger {
    logs: { level: string; messages: any[] }[] = [];

    info(...messages: any[]) {
        this.logs.push({ level: 'info', messages });
    }

    error(...messages: any[]) {
        this.logs.push({ level: 'error', messages });
    }

    // ... other methods

    clear() {
        this.logs = [];
    }
}

// In tests
const mockLogger = new MockLogger('Test');
const service = new UserService(mockLogger);

await service.createUser({ email: 'test@example.com' });

expect(mockLogger.logs).toContainEqual({
    level: 'info',
    messages: ['Creating user:', 'test@example.com']
});

Silent Logger

import { Logger, LogFlags } from '@bunty/common';

// Disable all logging in tests
const logger = new Logger('Test', LogFlags.None);

Best Practices

✅ Do

  • Create scoped loggers for each service/module
  • Use appropriate log levels (info, error, debug, etc.)
  • Log important state changes and operations
  • Include relevant context in log messages
  • Use structured data (objects) for complex information
  • Disable debug logs in production
  • Log errors with full stack traces
  • Use consistent naming for logger scopes

❌ Don’t

  • Log sensitive data (passwords, tokens, API keys)
  • Log excessively in hot paths (loops, frequent operations)
  • Use console.log() directly (use Logger instead)
  • Leave debug logs enabled in production
  • Log everything (be selective)
  • Ignore error logging
  • Use vague log messages (“something happened”)
  • Create new logger instances for every method call

Example: Complete Service

import { Injectable, Logger, LogFlags, env } from '@bunty/common';

@Injectable()
export class PaymentService {
    private readonly logger: Logger;

    constructor(
        private stripe: StripeAPI,
        private db: Database
    ) {
        this.logger = new Logger('PaymentService');

        // Configure based on environment
        if (env('NODE_ENV') === 'production') {
            this.logger.setFlags(LogFlags.Info | LogFlags.Success | LogFlags.Error);
        } else {
            this.logger.setFlags(LogFlags.All);
        }
    }

    async processPayment(orderId: number, amount: number) {
        this.logger.info('Processing payment:', { orderId, amount });

        try {
            // Validate order
            const order = await this.db.getOrder(orderId);
            this.logger.debug('Order fetched:', order);

            if (!order) {
                this.logger.warn('Order not found:', orderId);
                throw new Error('Order not found');
            }

            // Charge customer
            this.logger.info('Charging customer via Stripe');
            const charge = await this.stripe.charge({
                amount,
                currency: 'usd',
                source: order.paymentToken
            });

            this.logger.debug('Stripe response:', charge);

            // Update order
            await this.db.updateOrder(orderId, {
                status: 'paid',
                chargeId: charge.id
            });

            this.logger.success('Payment processed successfully', {
                orderId,
                chargeId: charge.id
            });

            return charge;
        } catch (error) {
            this.logger.error('Payment failed:', error);
            throw error;
        }
    }
}

📝 Logging Summary

Bunty’s built-in logger provides structured, colored console output with timestamps, scopes, and configurable log levels. Create scoped loggers for each service, control verbosity with bitwise flags, and get automatic formatting for objects, errors, and primitives. Perfect for development debugging and production monitoring without external dependencies.

Next Steps

Have questions? Join our Discord community
Found an issue? Edit this page on GitHub