Production-ready observability toolkit combining OpenTelemetry instrumentation with structured Pino logging for Node.js applications
⭐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.
npm install wonder-logger
yarn add wonder-logger
pnpm add wonder-logger
Comprehensive guides for all features:
📝 Structured Logging Guide - Complete Pino logger documentation
🔭 OpenTelemetry Guide - Telemetry instrumentation and tracing
withSpan⚙️ Configuration Guide - YAML-based configuration system
⭐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
level: ${LOG_LEVEL:-info}
redact:
- password
- token
transports:
# Console transport (JSON format in production)
- type: console
pretty: ${LOG_PRETTY:-false}
# File transport (relative paths resolve from config file location)
- type: file
dir: ./logs
fileName: app.log
# 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
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: ${PROMETHEUS_PORT:-9464}
- type: otlp
endpoint: ${OTEL_METRICS_ENDPOINT:-http://localhost:4318/v1/metrics}
exportIntervalMillis: 60000
instrumentation:
auto: true
http: true
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:
${VAR_NAME} or ${VAR_NAME:-default} syntaxExample:
# 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
endpoint: ${OTEL_ENDPOINT}
// Your application
import { createLoggerFromConfig, createTelemetryFromConfig } from 'wonder-logger'
const sdk = createTelemetryFromConfig()
const logger = createLoggerFromConfig()
See the complete Configuration Guide for:
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,
createFileTransport,
createOtelTransport
} from 'wonder-logger'
const logger = createLogger({
name: 'my-api',
transports: [
createConsoleTransport({ pretty: true, 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
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
loadConfig(options?: {
configPath?: string
required?: boolean
}): WonderLoggerConfig | null
// Load from specific file
loadConfigFromFile(filePath: string): WonderLoggerConfig
// Find config file in cwd
findConfigFile(fileName?: string): string | null
// Parse YAML with env var interpolation
parseYamlWithEnv(yamlContent: string): any
LOG_LEVEL=info # Minimum log level (trace, debug, info, warn, error, fatal)
# 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:
# 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
Use JSON in production
createConsoleTransport({ pretty: false })
Set appropriate log levels
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
Use OTLP for centralized collection
createOtelTransport({ serviceName: 'my-api' })
Include trace context for correlation
const logger = withTraceContext(baseLogger)
Use structured data, not string interpolation
logger.info({ userId, orderId }, 'Order placed') // Good
logger.info(`Order ${orderId} by ${userId}`) // Bad
Child loggers for scoped context
const requestLogger = logger.child({ requestId })
Configure sampling for high-volume services
createTelemetry({
tracing: { sampleRate: 0.1 } // 10% sampling
})
⭐Wonder Logger⭐ is written in TypeScript and provides complete type definitions:
import type {
// Logger types
LoggerOptions,
ConsoleTransportOptions,
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:
git checkout -b feature/my-featurepnpm test# 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:
Made with ✨ by jenova-marie