observability
v0.1.1@backendkit-labs/observability
Resilient structured logging, Prometheus metrics, and OTel spans.
Overview#
Observability is not three separate tools (logs, metrics, tracing) bolted together — it is the ability to understand the internal state of a system from its external outputs. In practice, most NestJS apps mix Winston, Prometheus, and OpenTelemetry with complex, fragile configurations where a saturated log ingester can degrade or crash the main API.
@backendkit-labs/observability solves this with an integrated, resilient approach. A single ObservabilityModule.forRoot() import gives you:
- →Automatic correlation ID propagation via AsyncLocalStorage — every log line in the entire async chain gets the same correlationId without manual threading.
- →Structured Winston logging enriched with serviceName, environment, correlationId, traceId, and spanId automatically.
- →Resilient telemetry transport: logs and metrics are sent in batches with a circuit breaker — if the ingester fails, the API is not affected.
- →Fire-and-forget metrics: MetricsService calls are async and non-blocking — they never add latency to your request path.
- →AllExceptionsFilter + ErrorMapper: centralizes HTTP error responses and maps domain exceptions to specific status codes declaratively.
Module Setup#
npm install @backendkit-labs/observability| Prop | Type | Description |
|---|---|---|
| serviceName | string | Service name stamped on every log line, metric, and span. |
| environment | string | Runtime environment (development, staging, production). |
| logLevel | 'debug'|'info'|'warn'|'error' | Minimum log level to output. |
| logTransport.url | string | HTTP endpoint to batch-ship logs. Protected by an internal circuit breaker. |
| logTransport.batchSize | number | Number of log entries per batch. |
| metrics.enabled | boolean | Expose a Prometheus /metrics HTTP scrape endpoint. |
| metrics.port | number | Port for the /metrics endpoint. |
| tracing.enabled | boolean | Enable OpenTelemetry tracing with W3C TraceContext propagation. |
| tracing.exporterUrl | string | OTLP gRPC endpoint for span export (e.g. http://otel-collector:4317). |
LoggerService#
Inject LoggerService anywhere. The correlation ID is propagated automatically via AsyncLocalStorage through the entire async call chain — no manual thread-local setup, no parameter threading. When OpenTelemetry tracing is enabled, traceId and spanId are appended too, making logs and traces directly correlatable.
@Injectable() export class PaymentService { constructor(private readonly logger: LoggerService) {} async charge(dto: ChargeDto) { this.logger.info('Processing charge', { userId: dto.userId, amount: dto.amount }); // Output: { "level":"info", "message":"Processing charge", // "userId":"usr_1", "amount":9900, "correlationId":"req-abc123", // "traceId":"4bf92f3...", "spanId":"00f067aa0ba902b7" } // Available levels: this.logger.debug('Verbose detail'); this.logger.warn('Unexpected but non-fatal condition'); this.logger.error('Something failed', { error: someError }); } }
MetricsService#
All metric operations are fire-and-forget — they never block the request path. The @TrackPerformance decorator automatically measures method duration, creates an OpenTelemetry span, and records success/error counters.
// Automatic duration tracking + OTel span @TrackPerformance({ operation: 'payment.charge' }) async charge(dto: ChargeDto): Promise<Result<Payment, PaymentError>> { ... } // Manual metric recording this.metrics.increment('payment.attempts', { currency: dto.currency }); this.metrics.increment('payment.failures', { reason: result.error.type }); this.metrics.histogram('payment.duration', durationMs, { status: 'ok' }); this.metrics.gauge('queue.depth', currentQueueSize);
OpenTelemetry#
When tracing.enabled is true, the module configures an OTLP gRPC exporter and propagates W3C TraceContext headers automatically on every outgoing request. Inject TracerService for manual span control.
@Injectable() export class PaymentService { constructor(private readonly tracer: TracerService) {} async charge(dto: ChargeDto): Promise<Result<Payment, PaymentError>> { return this.tracer.startActiveSpan('payment.charge', async (span) => { span.setAttributes({ 'payment.userId': dto.userId, 'payment.amount': dto.amount }); const result = await this.doCharge(dto); if (!result.ok) span.setStatus({ code: SpanStatusCode.ERROR, message: result.error.type }); span.end(); return result; }); } }
AllExceptionsFilter & ErrorMapper
Register AllExceptionsFilter globally to catch every unhandled exception and transform it into a consistent HTTP response shape. Use ErrorMapper to declaratively map domain errors to specific HTTP status codes — keeping business logic completely decoupled from HTTP concerns.
import { AllExceptionsFilter, ErrorMapper } from '@backendkit-labs/observability'; const mapper = new ErrorMapper() .map('not_found', 404) .map('forbidden', 403) .map('payment_failed', 402) .map('validation_error', 422); app.useGlobalFilters(new AllExceptionsFilter(mapper));
Examples#
From basic to production-grade — copy and adapt.
import { ObservabilityModule } from '@backendkit-labs/observability'; @Module({ imports: [ ObservabilityModule.forRoot({ serviceName: 'payment-api', environment: process.env.NODE_ENV ?? 'development', logLevel: 'info', metrics: { enabled: true, port: 9090 }, tracing: { enabled: true, exporterUrl: 'http://otel-collector:4317' }, }), ], }) export class AppModule {}
⚖️ vs. Alternatives#
Comparing against building your own observability stack from individual packages — which is what most teams do today.
| Feature | @backendkit-labs/observability | pino (alone) | Custom (winston + prom-client + OTel) |
|---|---|---|---|
| Auto correlation ID | ✅ Zero config | ❌ Manual AsyncLocalStorage | ❌ Manual |
| NestJS forRoot() module | ✅ | ❌ | ❌ |
| Prometheus /metrics | ✅ Built-in | ❌ | ⚠️ Requires prom-client setup |
| OpenTelemetry tracing | ✅ Built-in | ❌ | ⚠️ Requires @opentelemetry/* setup |
| @WithMetrics decorator | ✅ | ❌ | ❌ |
| Structured JSON logging | ✅ Winston | ✅ JSON | ✅ Depends |
| Setup complexity | ✅ One import | ⚠️ Medium | ❌ High |
| Runtime dependencies | ~5 (all bundled) | 0 | 5–10+ |
✅ Supported · ❌ Not supported · ⚠️ Partial / workaround needed. Download counts are approximate weekly npm averages.