@jenova-marie/wonder-logger - v2.0.15
    Preparing search index...

    @jenova-marie/wonder-logger - v2.0.15

    Wonder Logger Logo

    ⭐Wonder Logger⭐

    Production-ready observability toolkit combining OpenTelemetry instrumentation with structured Pino logging for Node.js applications

    Build Status codecov Node.js npm version TypeScript PRs Welcome License: MIT


    ⭐Wonder Logger⭐ is a comprehensive observability solution that unifies structured logging and distributed tracing for Node.js applications. Built on industry-standard tools (Pino and OpenTelemetry), it provides a clean, modular API for instrumenting your applications with production-grade observability.

    • Structured Logging - Fast, JSON-based logging with Pino
    • Distributed Tracing - Full OpenTelemetry SDK integration with automatic instrumentation
    • Metrics Collection - Prometheus and OTLP metrics exporters
    • Multiple Transports - Console, file, OpenTelemetry, and in-memory transports
    • Trace Context Correlation - Automatic injection of trace IDs into logs
    • Modular Architecture - Composable transports and exporters
    • Zero Globals - Factory pattern with no singleton state
    • Full TypeScript - Complete type definitions included
    • Production Ready - Battle-tested with 319 tests (unit + integration + E2E)
    • Grafana Loki - Log aggregation
    • Grafana Tempo - Distributed tracing
    • Jaeger - Distributed tracing
    • Prometheus - Metrics collection
    • Any OTLP-compatible backend (Honeycomb, Datadog, etc.)
    npm install wonder-logger
    
    yarn add wonder-logger
    
    pnpm add wonder-logger
    

    Comprehensive guides for all features:

    • ⚙️ Configuration Guide - START HERE! YAML configuration system

      • ⚠️ Type Safety Warnings - Critical info about environment variables with booleans/numbers
      • Complete schema reference for all configuration options
      • Environment variable interpolation syntax
      • Validation error troubleshooting with examples
    • 📂 Examples - Runnable demo scripts

      • Console output formats (pretty, minimal, aligned, JSON)
      • Child loggers for request-scoped context
      • OpenTelemetry trace context integration

    ⭐Wonder Logger⭐ uses YAML-based configuration for production deployments. For programmatic usage, see the Configuration Guide.

    1. Create wonder-logger.yaml in your project root:

    service:
    name: ${SERVICE_NAME:-my-api}
    version: ${SERVICE_VERSION:-1.0.0}
    environment: ${NODE_ENV:-development}

    logger:
    enabled: true # ⚠️ Boolean literal (NOT ${LOGGER_ENABLED:-true})
    level: ${LOG_LEVEL:-info}
    redact:
    - password
    - token
    transports:
    # Console transport (JSON format in production)
    - type: console
    pretty: false # ⚠️ Boolean literal (NOT ${LOG_PRETTY:-false})

    # File transport (async mode for better throughput)
    - type: file
    dir: ./logs
    fileName: app.log
    sync: false # Async I/O for long-running processes

    # Memory transport (for testing and runtime log inspection)
    - type: memory
    name: ${SERVICE_NAME:-my-api}
    maxSize: 10000
    level: debug

    # OpenTelemetry transport (send to Loki, etc.)
    - type: otel
    endpoint: ${OTEL_LOGS_ENDPOINT:-http://localhost:4318/v1/logs}

    plugins:
    # Inject trace_id and span_id into logs
    traceContext: true

    otel:
    enabled: true # ⚠️ Boolean literal (NOT ${OTEL_ENABLED:-true})
    tracing:
    enabled: true
    exporter: ${OTEL_TRACE_EXPORTER:-otlp}
    endpoint: ${OTEL_TRACES_ENDPOINT:-http://localhost:4318/v1/traces}
    sampleRate: 1.0
    metrics:
    enabled: true
    exporters:
    - type: prometheus
    port: 9464 # ⚠️ Number literal (NOT ${PROMETHEUS_PORT:-9464})
    - type: otlp
    endpoint: ${OTEL_METRICS_ENDPOINT:-http://localhost:4318/v1/metrics}
    exportIntervalMillis: 60000
    instrumentation:
    auto: true
    http: true

    ⚠️ CRITICAL: CLI applications require sync: true for file transports to prevent log loss when process exits quickly.

    1. Create wonder-logger.yaml in your project root:

    service:
    name: ${SERVICE_NAME:-my-cli}
    version: ${SERVICE_VERSION:-1.0.0}
    environment: ${NODE_ENV:-development}

    logger:
    enabled: true
    level: ${LOG_LEVEL:-info}
    transports:
    - type: console
    pretty: false

    # File transport - MUST use sync: true for CLI apps
    - type: file
    dir: ./logs
    fileName: cli.log
    sync: true # ⚠️ REQUIRED - prevents "sonic boom is not ready" crashes

    otel:
    enabled: false # Typically disabled for CLI tools

    Why sync: true? CLI apps often exit in <100ms (e.g., --help, validation errors), but async file transports take ~100-200ms to initialize. This race condition causes crashes. sync: true eliminates the race with only 4-5ms performance penalty.

    See Configuration Guide - Transport Configuration by Use Case for detailed recommendations.

    2. Load configuration in your application:

    import { createLoggerFromConfig, createTelemetryFromConfig } from 'wonder-logger'

    // Load from wonder-logger.yaml in project root
    const sdk = createTelemetryFromConfig()
    const logger = createLoggerFromConfig()

    // Now start logging with full observability
    logger.info('Application started')
    logger.info({ userId: 123 }, 'User logged in')

    3. Query memory logs programmatically:

    import { getMemoryLogs } from 'wonder-logger'

    // Query logs from last 5 minutes
    const recentLogs = getMemoryLogs('my-api', {
    since: Date.now() - 300000,
    level: ['error', 'warn'],
    format: 'parsed'
    })

    console.log(recentLogs)

    4. Environment variable syntax:

    # Required variable (throws error if not set)
    service:
    name: ${SERVICE_NAME}

    # Optional variable with default
    service:
    name: ${SERVICE_NAME:-my-api}
    version: ${npm_package_version:-1.0.0}

    For programmatic API usage, see Configuration Guide - Programmatic API.

    ⚠️ IMPORTANT: ⭐Wonder Logger⭐ uses Pino for structured logging. The data object MUST come BEFORE the message string:

    // ✅ CORRECT - Data object first, then message
    logger.info({ userId: 123 }, 'User logged in')
    logger.debug({ endpoint: '/api/users' }, 'Processing request')
    logger.error({ err, code: 'DB_ERROR' }, 'Database connection failed')

    // ❌ INCORRECT - Message first (data will be lost!)
    logger.info('User logged in', { userId: 123 }) // userId is NOT logged!
    logger.debug('Processing request', { endpoint: '/api/users' }) // endpoint is NOT logged!

    Why this matters: When you pass arguments in the wrong order, Pino treats the message string as the merge object and converts your data object to "[object Object]" in the message field. Your structured data is completely lost.

    API Signature:

    logger.info([mergingObject], [message], [...interpolationValues])
    logger.debug([mergingObject], [message], [...interpolationValues])
    logger.error([mergingObject], [message], [...interpolationValues])
    // All log levels follow this pattern

    This applies to all log levels: trace(), debug(), info(), warn(), error(), and fatal().

    Examples:

    // Data object only (no message)
    logger.info({ userId: 123, action: 'login' })

    // Message only (no data)
    logger.info('Server started')

    // Data object + message (most common)
    logger.info({ userId: 123 }, 'User logged in')

    // Data object + message + interpolation
    logger.info({ userId: 123 }, 'User %s logged in at %s', username, timestamp)

    // Error logging (err is serialized automatically by Pino)
    try {
    await riskyOperation()
    } catch (err) {
    logger.error({ err, userId: 123 }, 'Operation failed')
    }

    ⭐Wonder Logger⭐ uses YAML configuration files for production deployments, providing centralized configuration, environment variable interpolation, and easy multi-environment setup.

    Key benefits:

    • Environment flexibility - No code changes needed for different environments
    • Centralized settings - All configuration in one place
    • Variable interpolation - Use ${VAR_NAME} or ${VAR_NAME:-default} syntax
    • Validation - Automatic schema validation with helpful error messages
    • Version control friendly - Configuration as code

    Example:

    # wonder-logger.yaml
    service:
    name: ${SERVICE_NAME:-my-api}

    logger:
    level: ${LOG_LEVEL:-info}
    transports:
    - type: console
    pretty: false
    - type: memory
    name: ${SERVICE_NAME}
    maxSize: 10000

    otel:
    enabled: true
    tracing:
    exporter: otlp # Options: console, otlp, jaeger, none
    endpoint: ${OTEL_ENDPOINT}
    // Your application
    import { createLoggerFromConfig, createTelemetryFromConfig } from 'wonder-logger'

    const sdk = createTelemetryFromConfig()
    const logger = createLoggerFromConfig()

    See the complete Configuration Guide for:

    • Full schema documentation
    • Environment variable syntax
    • Multi-environment setup
    • Programmatic API (for dynamic configuration)
    wonder-logger/
    ├── Logger (Pino-based)
    │ ├── Transports
    │ │ ├── Console (with pretty printing)
    │ │ ├── File (async I/O)
    │ │ ├── OpenTelemetry (OTLP)
    │ │ └── Memory (queryable in-memory store)
    │ └── Plugins
    │ ├── Trace Context (OTEL correlation)
    │ └── Morgan Stream (HTTP request logging)

    └── OpenTelemetry
    ├── Trace Exporters
    │ ├── Console
    │ ├── OTLP (Tempo, Jaeger, Honeycomb)
    │ └── Jaeger
    ├── Metrics Exporters
    │ ├── Prometheus (pull-based)
    │ └── OTLP (push-based)
    └── Auto-Instrumentation
    └── HTTP, Express, GraphQL, databases, etc.
    import {
    createLogger,
    createConsoleTransport,
    createAlignedConsoleTransport
    } from 'wonder-logger'

    // Pretty console with all structured data (development)
    createConsoleTransport({ pretty: true, level: 'debug' })

    // JSON console output (production)
    createConsoleTransport({ pretty: false })

    // Minimal output - message only, no data clutter
    createConsoleTransport({
    pretty: true,
    prettyOptions: {
    hideObject: true,
    ignore: 'pid,hostname,name,service'
    }
    })

    // Aligned console - padded levels, single-line data with colorized keys/values
    // Output: [2025-12-01 04:00:57] INFO : User logged in {"userId":123}
    // Message in cyan, keys in gray, values in white
    createAlignedConsoleTransport({ level: 'debug' })

    // Aligned with inline key=value format (keys gray, values white)
    // Output: [2025-12-01 04:00:57] INFO : User logged in userId=123 action=login
    createAlignedConsoleTransport({ level: 'debug', dataFormat: 'inline' })

    // Aligned with data hidden (message only)
    createAlignedConsoleTransport({ level: 'debug', dataFormat: 'hidden' })

    // Disable colors for non-TTY environments
    createAlignedConsoleTransport({ level: 'debug', colorize: false })

    See examples/ for runnable demos of each console format.

    All console formats can also be configured entirely in YAML - no code required:

    logger:
    transports:
    # JSON output (default, production)
    - type: console
    variant: json
    level: info

    # Pretty multi-line output (pino-pretty)
    - type: console
    variant: pretty
    prettyOptions:
    colorize: true
    singleLine: false

    # Minimal output (message only, no data)
    - type: console
    variant: minimal
    minimalOptions:
    colorize: true

    # Aligned single-line with data (recommended for dev)
    - type: console
    variant: aligned
    alignedOptions:
    dataFormat: json # 'json' | 'inline' | 'hidden'
    colorize: true
    showTimestamp: true

    Available Variants:

    Variant Description Output
    json Raw JSON to stdout {"level":30,"msg":"User logged in","userId":123}
    pretty pino-pretty multi-line Multi-line colored output with indented data
    minimal Message only [timestamp] INFO: User logged in
    aligned Padded levels, colorized keys/values [timestamp] INFO : User logged in {"userId":123}

    Aligned variant color scheme:

    • Timestamp: Default terminal color
    • Level: Color-coded (INFO=green, WARN=yellow, ERROR=red, etc.)
    • Message: Cyan
    • Data keys: Gray
    • Data values: White
    import {
    createLogger,
    createAlignedConsoleTransport,
    createFileTransport,
    createOtelTransport
    } from 'wonder-logger'

    const logger = createLogger({
    name: 'my-api',
    transports: [
    createAlignedConsoleTransport({ level: 'debug' }),
    createFileTransport({ dir: './logs', fileName: 'app.log' }),
    createOtelTransport({
    serviceName: 'my-api',
    endpoint: 'http://localhost:4318/v1/logs'
    })
    ]
    })
    import express from 'express'
    import morgan from 'morgan'
    import {
    createLogger,
    createTelemetry,
    withTraceContext,
    createMorganStream,
    withSpan
    } from 'wonder-logger'

    // Initialize telemetry first
    createTelemetry({ serviceName: 'my-api' })

    // Create trace-aware logger
    const logger = withTraceContext(createLogger({ name: 'my-api' }))

    const app = express()

    // HTTP request logging
    app.use(morgan('combined', { stream: createMorganStream(logger) }))

    // Request-scoped logging
    app.use((req, res, next) => {
    req.logger = logger.child({ requestId: req.headers['x-request-id'] })
    next()
    })

    app.get('/users/:id', async (req, res) => {
    const user = await withSpan('fetch-user', async () => {
    req.logger.info({ userId: req.params.id }, 'Fetching user')
    return await db.users.findById(req.params.id)
    })

    res.json(user)
    })

    app.listen(3000, () => {
    logger.info({ port: 3000 }, 'Server started')
    })
    import {
    createLogger,
    createMemoryTransport,
    getMemoryLogs
    } from 'wonder-logger'

    const logger = createLogger({
    name: 'test',
    transports: [createMemoryTransport({ name: 'test-logs' })]
    })

    logger.info({ userId: 123 }, 'User action')

    // Query logs
    const logs = getMemoryLogs('test-logs', {
    level: 'info',
    format: 'parsed'
    })

    console.log(logs[0].userId) // 123
    import { withSpan } from 'wonder-logger'
    import { metrics } from '@opentelemetry/api'

    async function processPayment(orderId: string) {
    return withSpan('process-payment', async () => {
    // Your business logic
    const charge = await stripe.charges.create(...)

    // Record custom metrics
    const meter = metrics.getMeter('payments')
    const counter = meter.createCounter('payments_processed')
    counter.add(1, { status: 'success' })

    return charge
    })
    }
    // Create logger (programmatic)
    createLogger(options: LoggerOptions): pino.Logger

    // Create logger (config-driven)
    createLoggerFromConfig(options?: {
    configPath?: string
    required?: boolean
    overrides?: Partial<LoggerOptions>
    }): pino.Logger

    // Transports
    createConsoleTransport(options?: ConsoleTransportOptions): pino.StreamEntry
    createAlignedConsoleTransport(options?: AlignedConsoleTransportOptions): pino.StreamEntry
    createFileTransport(options?: FileTransportOptions): pino.StreamEntry
    createOtelTransport(options: OtelTransportOptions): pino.StreamEntry
    createMemoryTransport(options?: MemoryTransportOptions): pino.StreamEntry

    // Memory transport utilities
    getMemoryLogs(name: string, options?: MemoryQueryOptions): RawLogEntry[] | ParsedLogEntry[]
    clearMemoryLogs(name: string): void
    getMemoryLogSize(name: string): number
    getAllMemoryStoreNames(): string[]
    disposeMemoryStore(name: string): void

    // Memory transport streaming (RxJS)
    getMemoryLogStream(name: string): Observable<RawLogEntry> | null
    filterByLevel(level: string | string[]): OperatorFunction<RawLogEntry, RawLogEntry>
    filterSince(timestamp: number): OperatorFunction<RawLogEntry, RawLogEntry>
    withBackpressure(options: BackpressureOptions): OperatorFunction<RawLogEntry, RawLogEntry | RawLogEntry[]>

    // Plugins
    withTraceContext(logger: pino.Logger): pino.Logger
    createMorganStream(logger: pino.Logger): NodeJS.WritableStream
    // Create telemetry SDK (programmatic)
    createTelemetry(options: TelemetryOptions): TelemetrySDK

    // Create telemetry SDK (config-driven)
    createTelemetryFromConfig(options?: {
    configPath?: string
    required?: boolean
    overrides?: Partial<TelemetryOptions>
    }): TelemetrySDK

    // Manual instrumentation
    withSpan(spanName: string, fn: () => Promise<T>, tracerName?: string): Promise<T>

    // SDK methods
    sdk.start(): void
    sdk.shutdown(): Promise<void>
    sdk.forceFlush(): Promise<void>
    // Load and parse configuration (returns Result type)
    loadConfig(options?: {
    configPath?: string
    required?: boolean
    }): ConfigResult<WonderLoggerConfig>

    // Load from specific file (returns Result type)
    loadConfigFromFile(filePath: string): ConfigResult<WonderLoggerConfig>

    // Find config file in cwd (returns Result type)
    findConfigFile(fileName?: string): ConfigResult<string>

    // JSON parsing utilities (return Result types)
    parseJSONResponse<T>(text: string): JSONResult<T>
    validateJSONStructure<T>(data: unknown, requiredFields: string[]): JSONResult<T>
    extractJSON(text: string): string

    ⭐Wonder Logger⭐ v2.0 introduces type-safe error handling using ts-rust-result. Config loading and JSON parsing functions now return Result<T, E> types instead of throwing errors, enabling explicit error handling with full type safety.

    import {
    loadConfig,
    parseJSONResponse,
    validateJSONStructure,
    type ConfigResult,
    type JSONResult,
    type ConfigError,
    type JSONError
    } from 'wonder-logger'

    // Config loading returns Result<Config, ConfigError>
    const configResult = loadConfig()

    if (configResult.ok) {
    // TypeScript knows configResult.value is WonderLoggerConfig
    const config = configResult.value
    console.log('Config loaded:', config.service.name)
    } else {
    // TypeScript knows configResult.error is ConfigError
    const error = configResult.error

    // Pattern match on error kind
    switch (error.kind) {
    case 'FileNotFound':
    console.error('Config file not found:', error.context.path)
    break
    case 'InvalidYAML':
    console.error('Invalid YAML syntax:', error.message)
    break
    case 'MissingEnvVar':
    console.error('Missing env var:', error.context.varName)
    break
    default:
    console.error('Config error:', error.message)
    }
    }

    // JSON parsing returns Result<T, JSONError>
    const jsonResult = parseJSONResponse<{ status: string }>('{"status": "ok"}')

    if (jsonResult.ok) {
    console.log('Status:', jsonResult.value.status)
    } else {
    console.error('JSON parse failed:', jsonResult.error.message)
    }

    // JSON validation returns Result<T, JSONError>
    const data = { name: 'test', age: 30 }
    const validationResult = validateJSONStructure(data, ['name', 'age', 'email'])

    if (!validationResult.ok) {
    console.error('Missing fields:', validationResult.error.context.missingFields)
    }

    Exported Types and Utilities:

    // Core Result types
    import { ok, err, type Result } from 'wonder-logger'

    // Error types
    import { type ConfigError, type JSONError, type DomainError } from 'wonder-logger'

    // Error factory functions
    import {
    fileNotFound,
    fileReadError,
    invalidJSON,
    fromError,
    tryResultSafe,
    toSentryError
    } from 'wonder-logger'

    // Observability helpers
    import {
    toLogContext, // Convert error to Pino log context
    toSpanAttributes, // Convert error to OpenTelemetry span attributes
    toMetricLabels // Convert error to Prometheus metric labels
    } from 'wonder-logger'

    For comprehensive documentation on Result types and error handling patterns, see the ts-rust-result documentation.

    LOG_LEVEL=info  # Minimum log level (trace, debug, info, warn, error, fatal)

    # Color control (aligned console transport)
    NO_COLOR=1 # Disable colors in aligned console output
    FORCE_COLOR=0 # Disable colors (alternative syntax)
    # OTLP Exporter
    OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
    OTEL_EXPORTER_OTLP_HEADERS='{"x-api-key":"secret"}'

    # Jaeger Exporter
    JAEGER_ENDPOINT=http://localhost:14268/api/traces

    # Metrics
    OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:4318/v1/metrics

    # General
    NODE_ENV=production

    ⭐Wonder Logger⭐ includes comprehensive test coverage:

    • 237 unit tests - Fast, isolated component testing
    • 63 integration tests - Real behavior validation
    • 19 E2E tests - Production stack validation
    # Run all tests
    pnpm test

    # Run by category
    pnpm test:unit
    pnpm test:integration
    pnpm test:e2e

    # Coverage reports
    pnpm test:coverage
    pnpm test:unit:coverage

    # Watch mode
    pnpm test:watch
    • 30,000+ logs/second (JSON mode)
    • 20,000+ logs/second (pretty mode)
    • < 1ms per log entry
    • Async by default with background worker threads
    • Negligible overhead with batching
    • Configurable sampling rates
    • Background export workers
    1. Use JSON in production

      createConsoleTransport({ pretty: false })
      
    2. Set appropriate log levels

      level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
      
    3. Use OTLP for centralized collection

      createOtelTransport({ serviceName: 'my-api' })
      
    4. Include trace context for correlation

      const logger = withTraceContext(baseLogger)
      
    5. Use structured data, not string interpolation

      logger.info({ userId, orderId }, 'Order placed')  // Good
      logger.info(`Order ${orderId} by ${userId}`) // Bad
    6. Child loggers for scoped context

      const requestLogger = logger.child({ requestId })
      
    7. Configure sampling for high-volume services

      createTelemetry({
      tracing: { sampleRate: 0.1 } // 10% sampling
      })
    8. Use 'none' exporter to disable trace output while keeping context

      // Traces still work for log correlation, but no console/OTLP output
      createTelemetry({
      tracing: { exporter: 'none' }
      })
      # Or in YAML config
      otel:
      tracing:
      exporter: none # Spans created for context, but not exported

    ⭐Wonder Logger⭐ is written in TypeScript and provides complete type definitions:

    import type {
    // Logger types
    LoggerOptions,
    ConsoleTransportOptions,
    AlignedConsoleTransportOptions,
    FileTransportOptions,
    OtelTransportOptions,
    MemoryTransportOptions,
    MemoryQueryOptions,
    RawLogEntry,
    ParsedLogEntry,
    BackpressureOptions,

    // OpenTelemetry types
    TelemetryOptions,
    TracingOptions,
    MetricsOptions,
    TelemetrySDK,

    // Configuration types
    WonderLoggerConfig,
    ServiceConfig,
    LoggerConfig,
    OtelConfig,
    TransportConfig,
    LoggerPluginsConfig,
    TracingConfig,
    MetricsConfig,
    MetricsExporterConfig,
    PrometheusExporterConfig,
    OtlpMetricsExporterConfig,
    InstrumentationConfig
    } from 'wonder-logger'

    Your tsconfig.json should include:

    {
    "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "target": "ES2022"
    }
    }

    Contributions are welcome! Please follow these guidelines:

    1. Fork the repository
    2. Create a feature branch: git checkout -b feature/my-feature
    3. Write tests for your changes
    4. Ensure all tests pass: pnpm test
    5. Commit with descriptive messages
    6. Submit a pull request
    # Clone repository
    git clone https://github.com/jenova-marie/wonder-logger.git
    cd wonder-logger

    # Install dependencies
    pnpm install

    # Run tests
    pnpm test

    # Build
    pnpm build

    MIT © jenova-marie

    ⭐Wonder Logger⭐ is built on these excellent open-source projects:

    • Pino - Fast and low overhead logging
    • OpenTelemetry - Vendor-neutral observability framework
    • Grafana - Visualization and observability platform

    Made with ✨ by jenova-marie