npm.io
0.1.0 • Published 2d ago

@pronghorn/logger

Licence
WTFPL
Version
0.1.0
Deps
0
Size
15 kB
Vulns
0
Weekly
0

Logger

Logger is a lightweight, TypeScript-first structured logging middleware built as an external plugin for Pronghorn. It attaches a request-scoped logger with correlation IDs, configurable log levels, and pluggable output transports, replacing Pronghorn's bare-bones built-in logger middleware.

Built as a standalone package (@pronghorn/logger), the final piece of the @pronghorn/* ecosystem, designed for real observability, not just console lines.

Why Logger

Pronghorn's built-in logger middleware prints one line per request with no levels, no correlation IDs, and no way to route output anywhere but stdout. Logger replaces it with a proper structured logging layer suited for production observability stacks.

  • Every request gets a correlation ID (requestId), reused from an inbound header if present, otherwise generated, and echoed back in the response.
  • Four log levels (debug, info, warn, error) with configurable minimum threshold filtering.
  • JSON-line output by default, the format expected by log aggregators like Datadog and ELK, or a colorized pretty-print mode for local development.
  • Pluggable Transport functions, ship logs to console, file, or any custom sink (HTTP endpoint, message queue, etc.) simultaneously.
  • logger.child(meta) derives a scoped logger that merges extra context into every subsequent entry, used internally to attach requestId app-wide.
  • Zero runtime dependencies, pronghorn is only a peer dependency for types.

Installation

bun add @pronghorn/logger

Requires Bun >=1.3.0 and pronghorn >=0.1.2 as a peer dependency (used for typing the middleware only).

Quick Start

import { createApp } from 'pronghorn'
import { requestLogger } from '@pronghorn/logger'

const app = createApp()

app.use(requestLogger({ level: 'info' }))

app.get('/', context => context.json({ message: 'Hello, logged 📜' }))

await app.listen(4000)

Replaces Pronghorn's built-in logger middleware entirely, both are global scope, so swapping is a one-line change: drop logger, add requestLogger(...).

Core Concepts

Request-scoped logging

Once registered, every request gets a context.locals.logger instance already tagged with that request's correlation ID. Use it anywhere inside the handler instead of console.log.

app.post('/orders', context => {
  const logger = context.locals.logger as Logger
  logger.info('order:created', { orderId: 'ord_123', amount: 49.99 })
  return context.json({ created: true })
})
Automatic request/response logging

Every request automatically logs a request:start entry on entry and a request:end entry on exit, including method, path, status, and duration. The log level for request:end escalates automatically: info for 2xx/3xx, warn for 4xx, error for 5xx.

{"level":"info","message":"request:start","timestamp":"2026-07-18T10:28:00.000Z","requestId":"a1b2c3","meta":{"method":"GET","path":"/orders"}}
{"level":"info","message":"request:end","timestamp":"2026-07-18T10:28:00.042Z","requestId":"a1b2c3","meta":{"method":"GET","path":"/orders","status":200,"durationMs":42}}
Correlation IDs across services

The middleware reuses an inbound X-Request-Id header if present (useful behind a gateway or between microservices), otherwise generates a new UUID, and always echoes it back in the response header, letting you trace a single request across multiple hops.

app.use(requestLogger({ headerName: 'x-request-id' }))
Pretty console output for development

Switch to human-readable, color-coded output instead of JSON lines while developing locally.

import { prettyConsoleTransport } from '@pronghorn/logger'

app.use(requestLogger({ transports: [prettyConsoleTransport()] }))
2026-07-18T10:28:00.042Z INFO [a1b2c3] request:end {"method":"GET","path":"/orders","status":200,"durationMs":42}
Multiple transports at once

Pass an array to ship the same entries to multiple destinations simultaneously, e.g. JSON to a log file for aggregation and a pretty format to the terminal while developing.

import { jsonConsoleTransport, fileTransport } from '@pronghorn/logger'

app.use(requestLogger({
  transports: [jsonConsoleTransport(), fileTransport('./logs/app.log')]
}))
Custom transports

A Transport is just (entry: LogEntry) => void | Promise<void>, write your own to ship logs anywhere.

import type { Transport } from '@pronghorn/logger'

function httpTransport(endpoint: string): Transport {
  return async entry => {
    await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(entry)
    })
  }
}

app.use(requestLogger({ transports: [httpTransport('https://logs.example.com/ingest')] }))
Filtering by level

Only entries at or above the configured level are dispatched to transports, letting you silence debug noise in production while keeping it locally.

app.use(requestLogger({ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug' }))
Scoped child loggers

Derive a logger with extra baked-in context anywhere in your app, useful for tagging logs by subsystem (e.g. a background job runner) outside the request lifecycle.

import { createLogger } from '@pronghorn/logger'

const rootLogger = createLogger({ level: 'info' })
const jobLogger = rootLogger.child({ subsystem: 'email-queue' })

jobLogger.info('job:started', { jobId: 'job_42' })

Middleware Options

Option Type Default Description
level 'debug' | 'info' | 'warn' | 'error' 'info' Minimum level dispatched to transports
transports Transport[] [jsonConsoleTransport()] Output destinations, applied in order
pretty boolean false Shortcut for prettyConsoleTransport() when no transports are given
headerName string 'x-request-id' Header used to read/write the correlation ID

Built-in Transports

Export Description
jsonConsoleTransport() Writes each entry as a single JSON line to stdout/stderr
prettyConsoleTransport() Writes each entry as a colorized, human-readable line
fileTransport(filePath) Appends each entry as a JSON line to a file on disk

fileTransport appends only, external log rotation (e.g. logrotate) is expected for long-running production log files.

API Reference

requestLogger(options?: RequestLoggerOptions): Middleware - global middleware factory, register via app.use(requestLogger(options)).

createLogger(options?: LoggerOptions, baseMeta?): Logger - creates a standalone logger instance outside the request lifecycle.

Locals property Type Description
context.locals.logger Logger Request-scoped logger, pre-tagged with requestId
context.locals.requestId string The current request's correlation ID
Logger
Method Signature Description
debug (message, meta?) => void Logs at debug level
info (message, meta?) => void Logs at info level
warn (message, meta?) => void Logs at warn level
error (message, meta?) => void Logs at error level
child (meta) => Logger Returns a derived logger merging meta into every subsequent entry

Architecture

Logger is split into three modules, each with a single responsibility.

Module Responsibility
logger.ts Core Logger factory: level filtering, entry formatting, transport dispatch, child() derivation
transports.ts Built-in output sinks: JSON console, pretty console, and append-only file
middleware.ts Wires a per-request Logger into context.locals, generates/propagates the correlation ID, and logs start/end entries with automatic level escalation

Log entries are dispatched to every configured transport independently and asynchronously (void transport(entry)), so a slow file write never blocks the response, and one failing transport doesn't prevent others from receiving the entry.

Observability Notes

  • Use jsonConsoleTransport() in production so container log collectors (Docker, Kubernetes, systemd) can parse structured fields directly.
  • Always propagate the x-request-id header downstream when calling other services, so a single user request can be traced across your entire system.
  • Keep level: 'debug' local-only, debug-level logs are typically too noisy and sometimes too sensitive for production aggregation.
  • If using fileTransport, pair it with an external rotation tool, this package intentionally does not manage file rotation or retention itself.

License

WTFPL (Do What the Fuck You Want to Public License), see LICENSE for details.

Keywords