npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pronghorn/logger

v0.1.0

Published

Structured logging middleware for Pronghorn. Request-scoped correlation IDs, configurable log levels, JSON or pretty console output, and pluggable transports.

Downloads

39

Readme

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.