Skip to content

Repository files navigation

JellyLogger

A fast, flexible logging library built specifically for the Bun runtime.
JellyLogger provides structured logging with multiple transports, automatic redaction, and TypeScript-first design.

npm version npm downloads npm bundle size License: MIT TypeScript Bun GitHub stars GitHub issues GitHub last commit


✨ Features

  • πŸš€ Bun-Optimized: Built specifically for Bun runtime with native API integration
  • πŸ”Œ Multiple Transports: Console, File, Discord Webhook, and WebSocket support
  • 🎨 Flexible Formatters: JSON, Logfmt, NDJSON, and custom formatters
  • πŸ”’ Advanced Redaction: Comprehensive data protection with patterns and field-specific rules
  • πŸ‘Ά Child Loggers: Context inheritance with message prefixes and persistent context
  • 🌐 Bun HTTP Middleware: Dedicated request logger for Bun servers with field control
  • πŸ”„ File Rotation: Automatic log rotation with compression and date-based naming
  • πŸ“Š Structured Logging: Rich metadata and context support
  • πŸ›‘οΈ Internal Error Handling: Configurable handlers for transport and formatter errors
  • 🎯 TypeScript-First: Full type safety with extensive type definitions
  • ⚑ High Performance: Optimized for speed and memory efficiency
  • πŸ”§ Extensible: Plugin architecture for custom transports and formatters

πŸ“¦ Installation

bun add jellylogger

πŸš€ Quick Start

import { logger } from 'jellylogger';

// Basic logging
logger.info('Hello, JellyLogger!');
logger.error('Something went wrong', { error: 'Connection failed' });

// Structured logging with metadata
logger.info('User login', {
  userId: '12345',
  ip: '192.168.1.1',
  userAgent: 'Mozilla/5.0...',
});

// Using different log levels
logger.trace('Detailed debugging info');
logger.debug('Debug information');
logger.info('General information');
logger.warn('Warning message');
logger.error('Error occurred');
logger.fatal('Critical system error');

πŸ“ Log Levels

JellyLogger supports 7 log levels (0-6):

import { LogLevel } from 'jellylogger';

LogLevel.SILENT; // 0 - No logs
LogLevel.FATAL; // 1 - Critical errors
LogLevel.ERROR; // 2 - Errors
LogLevel.WARN; // 3 - Warnings
LogLevel.INFO; // 4 - Information
LogLevel.DEBUG; // 5 - Debug info
LogLevel.TRACE; // 6 - Detailed tracing

🎯 Multiple Transports

Console Transport (Default)

import { logger, ConsoleTransport } from 'jellylogger';

logger.addTransport(new ConsoleTransport());

File Transport with Rotation

import { logger, FileTransport } from 'jellylogger';

logger.addTransport(
  new FileTransport('app.log', {
    maxSize: '10MB',
    maxFiles: 5,
    compress: true,
    datePattern: 'YYYY-MM-DD',
  })
);

Discord Webhook Transport

import { logger, DiscordWebhookTransport } from 'jellylogger';

logger.addTransport(new DiscordWebhookTransport('https://discord.com/api/webhooks/...'));

WebSocket Transport

import { logger, WebSocketTransport } from 'jellylogger';

logger.addTransport(new WebSocketTransport('ws://localhost:8080/logs'));

Transport Presets

JellyLogger provides convenient preset functions:

import { useConsoleAndFile, useConsoleFileAndDiscord, useAllTransports } from 'jellylogger';

// Console + File
useConsoleAndFile('app.log');

// Console + File + Discord
useConsoleFileAndDiscord('app.log', 'https://discord.com/api/webhooks/...');

// All transports
useAllTransports('app.log', 'https://discord.com/api/webhooks/...', 'ws://localhost:8080/logs');

🎨 Formatters

Built-in Formatters

import { logger, createFormatter } from 'jellylogger';

// JSON formatter
logger.setOptions({
  format: createFormatter('ndjson'),
});

// Logfmt formatter
logger.setOptions({
  format: createFormatter('logfmt'),
});

// Default human-readable formatter
logger.setOptions({
  format: createFormatter('default'),
});

Custom Formatters

import type { LogFormatter, LogEntry } from 'jellylogger';

class CustomFormatter implements LogFormatter {
  format(entry: LogEntry): string {
    return `[${entry.levelName}] ${entry.message} ${JSON.stringify(entry.data || {})}`;
  }
}

logger.setOptions({ format: new CustomFormatter() });

πŸ”’ Data Redaction

Basic Redaction

logger.setOptions({
  redaction: {
    keys: ['password', 'token', 'secret', '*.apiKey'],
    replacement: '[REDACTED]',
  },
});

logger.info('User data', {
  username: 'alice',
  password: 'hunter2', // Will be [REDACTED]
});

Advanced Redaction with Patterns

logger.setOptions({
  redaction: {
    keys: ['password', '*.credentials.*'],
    keyPatterns: [/secret/i, /token/i],
    valuePatterns: [/\b\d{4}-\d{4}-\d{4}-\d{4}\b/], // Credit cards
    redactStrings: true,
    stringPatterns: [
      /Bearer\s+[\w-]+/gi, // Bearer tokens
      /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Emails
    ],
    whitelist: ['user.id'],
    redactIn: 'file', // Only redact in file logs
  },
});

Field-Specific Redaction

logger.setOptions({
  redaction: {
    fieldConfigs: {
      'user.email': {
        replacement: '[EMAIL_REDACTED]',
      },
      'debug.*': {
        disabled: true, // Never redact debug fields
      },
      'financial.*': {
        customRedactor: (value, context) => {
          return context.target === 'console' ? value : '[FINANCIAL_DATA]';
        },
      },
    },
  },
});

Custom Redaction Functions

logger.setOptions({
  redaction: {
    customRedactor: (value, context) => {
      if (context.path.includes('sensitive')) {
        return '[CUSTOM_REDACTED]';
      }
      return value;
    },
    auditHook: event => {
      console.debug(`Redacted ${event.type} at ${event.context.path}`);
    },
  },
});

πŸ‘Ά Child Loggers

Create contextual loggers that inherit parent configuration:

// Create child logger with prefix
const userLogger = logger.child({ messagePrefix: 'USER' });
userLogger.info('Login successful'); // [USER] Login successful

// Child logger with persistent context (v4.1.3+)
const requestLogger = logger.child({
  messagePrefix: 'REQUEST',
  context: { requestId: 'req-123', userId: 'user-456' },
});

requestLogger.info('Processing request');
// Context automatically included: { requestId: 'req-123', userId: 'user-456', ... }

// Nested child loggers merge context
const moduleLogger = requestLogger.child({
  messagePrefix: 'AUTH',
  context: { authMethod: 'jwt' },
});
moduleLogger.warn('Invalid token');
// [REQUEST] [AUTH] Invalid token
// Context: { requestId: 'req-123', userId: 'user-456', authMethod: 'jwt', ... }

🌐 Bun HTTP Request Logging

Dedicated middleware for logging Bun HTTP server requests:

import { bunRequestLogger } from 'jellylogger';

const handler = bunRequestLogger(
  async (req) => {
    // Your handler logic
    return new Response('Hello, World!');
  },
  {
    includeHeaders: true,
    includeBody: false,
    redactHeaders: ['authorization', 'cookie', 'x-api-key'],
    logLevel: 'info',
    messagePrefix: 'API',
  }
);

Bun.serve({
  port: 3000,
  fetch: handler,
});

Features:

  • Automatic request/response logging
  • Configurable field inclusion (headers, body, metadata)
  • Built-in header redaction
  • Client IP address capture
  • Non-blocking async logging
  • Full redaction config support

βš™οΈ Configuration

Global Logger Options

import { logger, LogLevel } from 'jellylogger';

logger.setOptions({
  level: LogLevel.INFO,
  format: createFormatter('ndjson'),
  colors: {
    info: 'blue',
    warn: 'yellow',
    error: 'red',
  },
  redaction: {
    keys: ['password', 'token'],
    redactIn: 'both',
  },
});

Transport-Specific Options

import { FileTransport } from 'jellylogger';

const fileTransport = new FileTransport('app.log', {
  maxSize: '50MB',
  maxFiles: 10,
  compress: true,
  datePattern: 'YYYY-MM-DD-HH',
});

logger.addTransport(fileTransport);

πŸ”„ File Rotation

Automatic log rotation with flexible configuration:

import { FileTransport } from 'jellylogger';

const transport = new FileTransport('logs/app.log', {
  maxSize: '100MB', // Rotate when file exceeds 100MB
  maxFiles: 30, // Keep 30 old files
  compress: true, // Compress rotated files with gzip
  datePattern: 'YYYY-MM-DD', // Daily rotation pattern
  auditFile: 'logs/.audit.json', // Track rotation events
});

πŸ›‘οΈ Internal Error Handling

Control how JellyLogger handles internal errors (transport failures, formatter issues, etc.):

import { setInternalErrorHandler, setInternalWarningHandler } from 'jellylogger';

// Register custom error handler
setInternalErrorHandler((message, error) => {
  // Send to monitoring service instead of console
  monitoringService.trackError({
    library: 'jellylogger',
    message,
    error: error instanceof Error ? error.message : String(error),
  });
});

// Register custom warning handler
setInternalWarningHandler((message, error) => {
  // Handle warnings differently
  console.warn(`[JellyLogger Warning] ${message}`, error);
});

// Errors in transports, formatters, redaction are now routed to your handlers
logger.addTransport(new WebSocketTransport('ws://invalid-url'));
logger.info('This continues to work'); // Error handled by your custom handler

Benefits:

  • Centralized error monitoring
  • Non-blocking failures (other transports continue working)
  • Integration with monitoring services
  • Consistent error handling across all JellyLogger operations

πŸ”Œ Creating Custom Transports

import type { Transport, LogEntry, TransportOptions } from 'jellylogger';
import { getRedactedEntry, logInternalError } from 'jellylogger';

class DatabaseTransport implements Transport {
  async log(entry: LogEntry, options?: TransportOptions): Promise<void> {
    try {
      // Apply redaction if needed
      const redacted = getRedactedEntry(entry, options?.redaction, 'file');

      // Store in database
      await this.database.insert('logs', {
        timestamp: redacted.timestamp,
        level: redacted.level,
        message: redacted.message,
        data: JSON.stringify(redacted.data),
      });
    } catch (error) {
      // Use internal error handler for consistent error reporting
      logInternalError('DatabaseTransport.log failed', error);
    }
  }

  async flush(): Promise<void> {
    // Flush any pending writes
    await this.database.flush();
  }
}

logger.addTransport(new DatabaseTransport());

πŸ§ͺ Testing

JellyLogger includes comprehensive test utilities:

import { MemoryTransport, resetAllMocks } from 'jellylogger/test-utils';

// In your tests
beforeEach(() => {
  resetAllMocks();
});

const memoryTransport = new MemoryTransport();
logger.addTransport(memoryTransport);

// Test logging
logger.info('test message');
expect(memoryTransport.logs).toHaveLength(1);
expect(memoryTransport.logs[0].message).toBe('test message');

πŸ“š Documentation


🀝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Run tests (bun test)
  4. Commit your changes (git commit -m 'Add amazing feature')
  5. Push to the branch (git push origin feature/amazing-feature)
  6. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.


πŸ™ Acknowledgments

  • Built for the Bun runtime
  • Inspired by popular logging libraries like Winston, Pino, and Bunyan
  • TypeScript-first design for optimal developer experience

Made with ❀️ for the Bun ecosystem

About

Resources

Code of conduct

Contributing

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages