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

@easyweb/logging

v1.0.2

Published

Shared structured logging for Easyweb microservices: one pino configuration, request-scoped context over AsyncLocalStorage, an HTTP access log, and the redaction every service needs before its logs leave the pod

Downloads

5,655

Readme

@easyweb/logging

One pino configuration for every Easyweb service, request-scoped context that does not have to be threaded by hand, an HTTP access log, and the redaction a log needs before it leaves the pod.

Why it exists

src/lib/logger.ts was copy-pasted into 21 services. It had drifted into two variants; domain and moderation both defaulted their service name to "billing-service"; and seven services were missing the guard that stops pino-pretty's worker thread making Jest exit 1 under a green report.

The bigger problem was correlation. getLogger(req) returned a child bound to requestId and had to be passed down as an explicit log parameter. Roughly 100 call sites did that; the other ~1,300 logged through the module-level root logger and carried no request id at all.

The mixin

createServiceLogger installs a pino mixin that reads an AsyncLocalStorage store. Every existing call site gains the ambient fields with no edit:

// unchanged call site
log.info({ projectId }, "Project created");

// what it now emits
{ "service": "project-service", "requestId": "…", "userId": "…", "projectId": "…" }

pino merges the mixin UNDER the object the call site passed, so an explicit field always wins over an ambient one.

The mixin hands pino a COPY of the scope (1.0.2). pino's default merge is Object.assign(mixinObject, mergeObject), and 1.0.1 returned the live store — so every call's fields were written into the request's context and appeared on every later line of that request, an err included. It also skips any key the logger already binds, which is why a getLogger(req) line or a subscriber's child({ requestId, queue }) line now carries requestId once, not twice.

Usage

// src/lib/logger.ts
import { createServiceLogger } from "@easyweb/logging";
import config from "../config";

const { logger, getLogger } = createServiceLogger({
  serviceName: config.serviceName,
  logLevel: config.logLevel,
});

export { getLogger };
export default logger;
// src/app.ts — the access log needs the scope, so it goes after requestContext
app.use(requestContext);
app.use(createHttpLogger(logger));

Opening a scope outside a request — a BullMQ job, a broker handler:

await runWithContext({ jobId: job.id, job: job.data.type }, () => handle(job));

Adding to a scope already running — authenticate does this once it has verified the token:

bindContext({ userId: decoded.sub });

The access log

One line per finished request, msg: "request".

route is the route pattern (/billing/me/invoices/:invoiceId), and that is a contract rather than a convenience: the RED metrics are recording rules over it, so a per-id value would make the series unbounded. path carries the real path beside it, from req.originalUrl — Express strips the mount prefix off req.path during router dispatch, so reading that in the finish callback names a route that does not exist.

The query string is never logged. ?token=… on the verification route is a live credential.

/health, /livez, /readyz and /metrics are skipped — the kubelet would otherwise make the readiness probe the largest log stream in the cluster.

Redaction

Two mechanisms, covering different shapes:

  • REDACT_PATHS — pino's fixed-path redaction, for our own payloads. Passwords, tokens, cookies and auth headers, at the top level and one level down.
  • scrubResponseData — a recursive, depth- and size-capped walk over err.response.data, for payloads we do not control. Xendit, Stripe, Resend, Meta, Apify, Cloudflare and Gitea all put personal data in an error body, and 407 logger.error({ err }) call sites fed it into the log verbatim and uncapped.

err.response.data is capped at 2 KB after scrubbing, not instead of it — truncating first would keep whatever fitted, which for a failed charge is the payer block.

What is deliberately NOT redacted

Email addresses. auth-service logs one on three lines — signup, an operator-create refusal, and a Google registration — and they are the signup audit trail: userId alone cannot answer "which address did they sign up with". The exposure is real and bounded by log retention. See docs/platform/ADR-0001.