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

@noego/logger

v0.3.1

Published

A flexible logging library for Node.js

Readme

@noego/logger

Lightweight logging for Node.js with:

  • level filtering (TRACE to FATAL)
  • named loggers
  • contextual metadata
  • pluggable transports
  • optional @noego/ioc integration

Install

npm install @noego/logger

Quick Start

import { getLogger, configureLogging, LogLevel } from "@noego/logger";

configureLogging({
  level: LogLevel.INFO,
  serviceName: "my-service",
});

const logger = getLogger("api");
logger.info("Server started", { port: 3000 });
logger.error("Request failed", new Error("boom"));

Context and Named Loggers

const base = getLogger("http").withContext({ requestId: "req_123" });

base.info("Incoming request");
base.named("auth").warn("Invalid token", { userId: "u_42" });

Ambient Log Context

withContext() above is static: you build a child logger and pass it around. Ambient context is the dynamic complement — fields attached to the current execution flow that every logger.* call inside that flow carries automatically, with no logger or id threaded through function signatures.

import { extendLogContext, getLogger } from "@noego/logger";

const logger = getLogger("orders");

// somewhere at the edge of a request
extendLogContext({ requestId: "r_1", route: "/orders/{id}" });

// anywhere deeper in the same flow — services, repositories, after awaits
logger.info("order loaded", { orderId: 42 });
// → context: { requestId: "r_1", route: "/orders/{id}", orderId: 42 }

Nothing here needs async: fields are set synchronously and follow the flow across await, timers and I/O.

Where fields live

| Layer | Set by | Lifetime | |---|---|---| | Scope bag | extendLogContext(fields) while a @noego/ioc execution scope is active | the scope — a request, a queue invocation, a test scope | | Overlay | runWithLogContext(fields, fn), withLogContext(...), @LogContext(...) | the wrapped call's dynamic extent (inherit + extend, never leaks upward) |

currentLogContext() returns scope bag ← overlays merged, innermost wins. The scope bag is keyed by the active ioc scope object (read from the channel @noego/ioc anchors on globalThis; ioc is not imported), so two sibling ExecutionContext.run(scope, …) calls see the same fields, and two physical copies of @noego/logger share everything. With no ioc scope, overlays alone act as the base. With neither, there is no context and records are unchanged.

Sticky vs scoped

// sticky: rest of the flow, including code that already returned to the caller
extendLogContext({ correlationId: body.correlationId });

// scoped: only inside the callback
await runWithLogContext({ itemId }, () => process(item));

// scoped, for a plain function
export const handle = withLogContext((req: Req) => ({ requestId: req.id }), async (req: Req) => { ... });

// scoped, for a method — a static object or a function of the call's arguments
class Controller {
  @LogContext({ action: "health" })
  health() { ... }

  @LogContext((req: Req) => ({ correlationId: req.body?.correlationId }))
  async login(req: Req) { ... }
}

extendLogContext returns false (and does nothing) when there is nothing to attach to; it never throws. @LogContext supports both legacy experimentalDecorators and TC39 decorators.

How a record is built

| Call | Emitted context | |---|---| | logger.info("m") | { ...ambient } | | logger.info("m", { outcome: "ok" }) | { ...ambient, outcome: "ok" } — flat; call fields win on conflict | | logger.error("m", err) | { ...ambient, args: { name, message, stack } } | | logger.info("m", "text") / ("m", 1, 2) | { ...ambient, args: "text" } / { ...ambient, args: [1, 2] } | | logger.withContext({...}).info("m") | { ...ambient, ...withContextData }withContext wins | | no ambient context | unchanged from earlier versions |

Per-call fields are never sticky. logger.info("m", { outcome }) describes that one event; to make a field follow the flow use extendLogContext or a scoped form above.

In a @noego/app product

Fields come from the seam that knows them, so controllers need no annotation for the common ones:

// server.ts boot hooks
requestScope: (scope, { request }) => {
  extendLogContext({ requestId: crypto.randomUUID(), method: request.method });
},
onRouteMatched: ({ route }) => {
  // raw pattern from Dinner/Forge, e.g. "/v1/connect/auth/{action}" — before body parsing
  extendLogContext({ route: route.path, action: route.action });
},
// body-derived fields: the parser (one place) or @LogContext on the method

In testApp, env.request() / env.host.handle() go through the real host and get all of this; env.dinner.controller(X) builds an instance outside any request and deliberately gets no ambient fields.

IoC Integration

import { createContainer } from "@noego/ioc";
import { registerLoggerFactory } from "@noego/logger/ioc";

const container = createContainer();
registerLoggerFactory(container, Symbol.for("app:logger"), "app");

API

  • getLogger(name) -> logger instance
  • configureLogging(options) -> configure global manager
  • shutdown() -> close all transports
  • ConsoleTransport -> default console transport implementation
  • extendLogContext(fields) -> attach fields to the current flow (sticky); false if nothing to attach to
  • runWithLogContext(fields, fn) -> run fn with fields layered on (scoped)
  • withLogContext(fieldsOrDerive, fn) -> wrap a function so each call is scoped
  • LogContext(fieldsOrDerive) -> method decorator form of withLogContext
  • currentLogContext() -> the merged fields, or undefined

Environment Variables

  • LOG_LEVEL (default: inferred by library, typically INFO)
  • SERVICE_NAME (default: noego)
  • NODE_ENV (default: development)