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

@zebra-web/observability

v1.0.0

Published

Readme

@zebra-web/observability

Zero-dependency observability middleware for zebra apps: request id, access logs, error reporting, metrics and health endpoints. Pure middleware — nothing here touches @zebra-web/core or ships its own logger; you bring the sinks.

Install

bun add @zebra-web/observability

Quick start

import { Zebra } from "@zebra-web/core";
import { accessLog, errorReporter, health, metrics, requestId } from "@zebra-web/observability";

const app = new Zebra();

app.use(requestId()); // first: everything below can correlate on the id
app.use(accessLog()); // console.log per request
app.use(errorReporter((error, req, info) => {
  console.error("handler failed", error, info.requestId);
}));
const metricsHandle = metrics({ onSample: (s) => console.log(s) });
app.use(metricsHandle);
app.use(health({ readiness: () => db.ping() }));

app.get("/", () => Response.json({ ok: true }));

Middleware order matters: requestId must be registered before accessLog, errorReporter and metrics so they can read the id from req.ctx. Health probes are ordinary requests, so registering health inside the observability stack logs and counts them too.

requestId

requestId({ headerName?, generator?, propagate? }) keeps a client-provided x-request-id (or the configured header) or generates one (crypto.randomUUID by default), stores it on req.ctx and echoes it on the response header (propagate, default true).

import { getRequestId, requestId } from "@zebra-web/observability";

app.use(requestId({ headerName: "x-trace-id" }));
app.get("/", (req) => Response.json({ id: getRequestId(req) }));

getRequestId(req) returns the id for the current request, or undefined if the middleware did not run (e.g. inside raw dispatch paths). Note: responses produced by core's error middleware (thrown handlers) are built after this middleware unwinds, so the error response does not carry the x-request-id header — correlate via the access log / error reporter, which do see it.

accessLog

accessLog({ writer? }) emits one entry per request via the writer (default: a single-line console.log):

export interface AccessLogEntry {
  method: string;
  path: string;
  status: number | undefined; // undefined when the handler threw
  durationMs: number;
  requestId: string | undefined;
  timestamp: number;
  error?: unknown;            // set when the handler threw
}

app.use(accessLog({
  writer: (entry) => sink.write(JSON.stringify(entry)),
}));

Errors are recorded on the entry and rethrown unchanged — core still converts them into the Problem+Json response, and a throwing writer never breaks the request.

errorReporter

errorReporter(reporter) wraps next() so it observes thrown errors before core's error middleware converts them to Problem+Json. The error is always rethrown unchanged and a throwing reporter never masks it.

app.use(errorReporter((error, req, info) => {
  sentry.captureException(error, { extra: info }); // { method, path, requestId }
}));

metrics

metrics({ onSample?, maxLatencySamples? }) counts requests, errors (thrown or status >= 500) and in-flight concurrency (with its peak), and keeps a fixed latency histogram plus a bounded sample window for p50/p95. The middleware doubles as a handle:

const m = metrics({ onSample: (s) => pushToPrometheus(s) });
app.use(m);

const snapshot = m.snapshot();
// { totalRequests, errors, inFlight, peakInFlight,
//   latency: { bucketBoundsMs, buckets }, latencySamples, latencyP50, latencyP95 }

Histogram buckets (ms): [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, ∞]. Latency samples are capped at maxLatencySamples (default 1000), so memory stays bounded; percentiles are nearest-rank over that window. A throwing onSample never breaks the request.

health

health({ path?, readinessPath?, liveness?, readiness? }) answers liveness on /healthz (default path) and readiness on /readyz (default readinessPath) with {"status":"ok"} / 200 or {"status":"unavailable"} / 503. All other paths pass through. Probes are user callbacks — no business logic lives here; default probes are always healthy. A throwing probe is treated as unavailable (and logged), so health endpoints always answer.

app.use(health({
  readiness: async () => (await db.ping()) === "OK",
}));