Logging is one of those fundamental concerns that every application must address, yet it's often treated as an afterthought. In the JavaScript and TypeScript ecosystem, developers face a bewildering array of logging libraries: Pino, Winston, Bunyan, and countless others. Each library has its own API, quirks, and conventions. This fragmentation creates real problems: inconsistent codebases, vendor lock-in, and the constant need to rewrite logging code when requirements change.
LogLayer solves these problems by providing a unified abstraction layer that sits on top of any logging library while offering a consistent, fluent API that makes logging both powerful and pleasant to use.
The Problem with Traditional Logging
Before diving into LogLayer's solutions, let's examine the problems it addresses. Most logging libraries share a common set of methods like info, warn, and error, but they diverge significantly in how they handle structured metadata and error objects.
Consider the inconsistency across popular libraries:
// Winston: message first, then metadata
winston.info('User logged in', { userId: '123', ip: '192.168.1.1' })
// Bunyan: metadata first, then message
bunyan.info({ userId: '123', ip: '192.168.1.1' }, 'User logged in')
// Pino: similar to Bunyan but with different error handling
pino.info({ userId: '123', err: error }, 'User logged in')
This inconsistency means that switching between logging libraries requires rewriting every log statement in your codebase. It also means that teams working across multiple projects must remember different APIs, leading to mistakes and reduced productivity.
Error handling is particularly problematic. Some libraries expect error objects as the first parameter, others as a property in a metadata object, and still others require manual serialization. This leads to ad-hoc solutions, inconsistent error logging, and lost stack traces.
Consistent API and Easy Swapping
The most immediate benefit of LogLayer is consistency. Regardless of whether you're using Pino, Winston, Bunyan, or any other supported transport, your logging code remains identical:
import { LogLayer } from 'loglayer'
import { PinoTransport } from '@loglayer/transport-pino'
import pino from 'pino'
const log = new LogLayer({
transport: new PinoTransport({ logger: pino() })
})
// This code works the same way regardless of the transport
log.withMetadata({ userId: '123', ip: '192.168.1.1' })
.withError(new Error('Connection failed'))
.error('User login failed')
If you later decide to switch to Winston or a cloud provider transport, you only change the transport configuration. Your application code remains untouched. This consistency extends across your entire codebase and different projects, reducing cognitive load and eliminating API-related bugs. This flexibility is especially valuable in microservices architectures where different services might benefit from different logging backends, or when migrating legacy applications to modern logging solutions.
Multi-Transport Support
LogLayer doesn't limit you to a single logging destination. You can send logs to multiple transports simultaneously, enabling powerful use cases:
import { LogLayer } from 'loglayer'
import { PinoTransport } from '@loglayer/transport-pino'
import { DatadogTransport } from '@loglayer/transport-datadog'
import pino from 'pino'
const log = new LogLayer({
transport: [
new PinoTransport({ logger: pino() }), // Local logging
new DatadogTransport({ ... }) // Cloud logging
]
})
// Single log call sends to both destinations
log.info('Request processed')
This multi-transport capability enables gradual migrations, A/B testing of logging backends, and redundancy for critical applications. You can ship logs directly to cloud providers like DataDog or New Relic without relying on their APM packages or sidecars, giving you more control over your observability pipeline.
Fluent API for Better Developer Experience
LogLayer's fluent API makes logging code more readable and maintainable. The separation of message, metadata, and errors into distinct method calls creates a clear, chainable interface:
log.withContext({ requestId: 'abc123', userId: '456' })
.withPrefix('[API]')
.withMetadata({ endpoint: '/users', method: 'GET' })
.withError(error)
.error('Request failed')
This approach has several advantages. First, it's self-documenting: the code clearly shows what context is being added, what metadata is included, and what error is being logged. Second, it's composable: you can build up log statements incrementally, making it easy to add or remove information. Third, it prevents common mistakes like passing errors in the wrong format or forgetting to include metadata.
The fluent API also supports context that persists across multiple log calls, which is invaluable for request-scoped logging. LogLayer provides sophisticated context management through its context manager system, supporting isolated contexts, linked contexts, and custom context managers for advanced use cases like async context propagation:
// Set context once for a request
const requestLogger = log.withContext({ requestId: 'abc123', userId: '456' })
// All subsequent logs include this context
requestLogger.info('Processing started')
requestLogger.withMetadata({ step: 'validation' }).info('Validating input')
requestLogger.withMetadata({ step: 'database' }).info('Querying database')
This context management is particularly valuable in serverless environments, microservices, and any application where you need to correlate logs across asynchronous operations.
Standardized Error Handling
Error handling is one of LogLayer's strongest features. It provides consistent error serialization across all transports, ensuring that error objects are always properly formatted with their stack traces, messages, and types preserved:
log.withError(new Error('Database connection failed')).error('Operation failed')
This works the same way regardless of whether you're using Pino (which has good error support), Winston (which requires configuration), or a cloud provider transport. LogLayer handles the serialization automatically, ensuring that stack traces are never lost and error information is always structured consistently.
Powerful Plugin System
LogLayer's plugin system allows you to extend and modify logging behavior at various points in the log lifecycle. Plugins can transform data, filter logs, enrich metadata, and more:
import { redactionPlugin } from '@loglayer/plugin-redaction'
const log = new LogLayer({
transport: new PinoTransport({ logger: pino() }),
plugins: [
redactionPlugin({
paths: ['password', 'apiKey', 'token'],
censor: '[REDACTED]'
})
]
})
// Sensitive data is automatically redacted
log.withMetadata({ password: 'secret123', userId: '456' }).info('User login')
The plugin system is extensible, allowing you to create custom plugins for your specific needs. Official plugins include redaction for sensitive data, filtering for log levels, OpenTelemetry integration, and DataDog APM trace injection.
Comprehensive Transport Ecosystem
LogLayer supports an extensive ecosystem of transports covering virtually every logging need:
- Traditional Logging Libraries: Pino, Winston, Bunyan, Log4js, Roarr, Tracer, TSLog, Consola, Signale, Loglevel, Logtape
- Cloud Providers: DataDog, New Relic, Google Cloud Logging, AWS CloudWatch Logs, Sumo Logic, Axiom, Better Stack, Dynatrace, Victoria Logs
- Specialized Systems: OpenTelemetry, Sentry, Electron Log, AWS Lambda Powertools, Log File Rotation, Pretty Terminal, HTTP
- Browser Environments: DataDog Browser Logs, Console Transport
The transport system is extensible, allowing you to create custom transports for proprietary or specialized logging systems.
Testing Support
LogLayer includes built-in testing utilities that make it easy to verify logging behavior in tests:
import { MockLogLayer } from 'loglayer'
const log = new MockLogLayer()
// Your application code uses log normally
myFunction(log)
// Verify logs were called correctly
expect(log.info).toHaveBeenCalledWith('Expected message')
The mock implementation ensures that no actual logging occurs during tests, keeping test output clean and tests fast. This is especially valuable in large codebases where logging is pervasive.
Conclusion
Logging is too important to be fragmented across incompatible APIs and locked into single-vendor solutions. LogLayer provides the abstraction layer that modern applications need: a consistent, powerful API that works with any logging backend, in any environment, with any runtime.
Whether you're starting a new project or migrating an existing one, LogLayer gives you the freedom to choose the best logging solution for each use case while maintaining consistency across your codebase. It's not just another logging library. It's the abstraction layer that makes all logging libraries work together seamlessly.
LogLayer is open source and available under the MIT license. You can find the source code, contribute, and learn more at the LogLayer GitHub repository or visit the official documentation to get started.
Top comments (0)