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

@pgfsm/logging

v0.2.0

Published

Shared LogTape logging configuration (composition-root configurator, category vocabulary, table-rendering console sink) for FSM services

Readme

fsm-logging-ts — Shared Logging Foundation

The single owner of logging config for the repo. A leaf package (depends only on @logtape/*) that wraps LogTape with one configurator, a shared category vocabulary, and a console sink that renders attached data in the most readable shape (table / dir / log).

What it does

  • configureLogging(opts) — the one process-wide LogTape configurator
  • CATEGORY — canonical namespaces (api, worker, db, compiler, fsmlet) so every package speaks the same category vocabulary
  • LogLevel — LogTape's severities plus "silent" (routed to a noop sink)
  • A console sink (getTableConsoleSink) that formats the message line with LogTape's built-in ANSI/text formatters, and renders values attached via table() / dir() / inspect() as console.table / console.dir
  • Optional OpenTelemetry sink alongside the console (otel: true)

The golden rule

configureLogging() is called exactly once per process, at the entry point. Calling LogTape's configure() twice in one process throws.

  • Apps/CLIs configure — each composition root (API deno.ts, each worker/ compiler CLI, test files) resolves levels from its own validated env and calls configureLogging() once.
  • Libraries never configure — they only getLogger([CATEGORY.x, ...]) from @logtape/logtape. A library that is also a CLI (e.g. the compiler) configures only when it is the process root, never on import.
  • Env is read at the composition root, not here — this package is env-agnostic and takes explicit levels (dependency injection).

Configuring at an entry point

import {
  CATEGORY,
  configureLogging,
  isTerminal,
  type LogLevel,
} from "@pgfsm/logging";

// Resolve levels from your OWN validated env, then configure once.
await configureLogging({
  levels: {
    [CATEGORY.api]: "info",
    [CATEGORY.worker]: "debug",
    [CATEGORY.db]: "warning",
    [CATEGORY.compiler]: "silent", // routed to a noop sink — nothing emitted
  },
  otel: true, // optional; OTLP endpoint read by the OTel SDK from env
});

Only the categories you list are configured. To add a category's logs to a process, add [CATEGORY.x]: level to that entry point's call — do not add a second configure(). isTerminal is exported so a root can, e.g., force "debug" on a TTY.

By default the console sink splits by level: warning and above go to stderr, info/debug to stdout. A CLI whose stdout is data (e.g. pgfsmctl … -o json | jq) passes consoleStream: "stderr" to send every level to stderr; attached data (table()/dir()/extra properties) is then written as one JSON line on stderr, since console.table/console.dir can only write stdout.

Logging in a library

Libraries import nothing from this package except CATEGORY — they use LogTape's getLogger directly:

import { getLogger } from "@logtape/logtape";
import { CATEGORY } from "@pgfsm/logging";

const logger = getLogger([CATEGORY.compiler, "validate"]);

logger.info("Validated {count} folders", { count: 12 });

Rendering data: table / dir / inspect

Attach a value with an opt-in helper as the second argument to a log call. Each returns a LogTape properties object, so it merges with normal structured fields. The sink renders the attached value beneath the message line on a TTY; piped/non-TTY output emits it as a single greppable JSON line instead.

import { dir, inspect, table } from "@pgfsm/logging";

// force a table — flat object prints as key/value rows
logger.info(
  "Database config",
  table({ host: "localhost", port: 5432, ssl: true }),
);

// narrow the columns (2nd arg): array columns, or a flat-object key allow-list
logger.info("Results", table(rows, ["id", "name"]));

// force console.dir with full depth — best for nested objects
logger.info(
  "Incoming request",
  dir({ method: "POST", body: { name: "Alice" } }),
);

// let the sink decide from the value's shape (see table below)
logger.info("Users", inspect(users));

Use table() when you want a specific mode or to narrow columns; otherwise you can rely on the auto behavior below.

Auto-render from a bare property (no helper)

You don't have to wrap data in a helper at all. Any extra property — one whose value is a non-primitive (array/object) and that is not interpolated into the message template — is auto-rendered beneath the line with its key as a label, using the same shape decision as inspect():

logger.info("All folder validation results ({count}):", {
  count: allFolderResults.length,
  allFolderResults,
});
All folder validation results (5):
allFolderResults
┌───────┬──────────┬────────────┐
│ (idx) │ fsmName  │ fsmVersion │
│  ...  │   ...    │    ...     │
└───────┴──────────┴────────────┘

Rules that keep this from becoming noisy:

  • Interpolated properties are skipped. count is referenced as {count} in the template, so it renders inline ((5)) and is not tabled. This means the common logger.error("failed: {error}", { error: err }) pattern shows the error in the message line only — never double-rendered.
  • Scalars are skipped. Only arrays/objects auto-render; a bare requestId: "abc" stays in the structured record (for OTel/files). Put scalars you want on-screen in the message template via {placeholder}.
  • TTY only. Auto-render is for interactive terminals; piped output leaves extra properties in the record for structured sinks and doesn't echo them.

Reach for the explicit table() / dir() / inspect() helpers when you need to force a mode, narrow columns, or render a value that is also interpolated.

Decision table (inspect / auto mode)

| Value | Rendered with | | ----------------------------------- | -------------------------------- | | Array (of objects or of primitives) | console.table | | Flat object (all primitive values) | console.table (key/value rows) | | Nested object | console.dir({ depth: null }) | | string / number / boolean | console.log |

table() and dir() force their mode; inspect() applies the table above. On a non-TTY, the attached value is emitted as one JSON line regardless of mode.

Why a custom sink

LogTape's built-in getConsoleSink dispatches only to console.log/info/warn/error (chosen by log level) — it can never call console.table or console.dir. So the shape-based routing needs a custom sink. Everything else reuses the library: the message line comes from LogTape's built-in getAnsiColorFormatter (TTY) / getTextFormatter (non-TTY).

Key files

| File | Purpose | | ------------------- | -------------------------------------------------------- | | src/configure.ts | configureLogging() — the one process-wide configurator | | src/sink.ts | Console sink: built-in formatter + table/dir/log routing | | src/render.ts | table() / dir() / inspect() payload builders | | src/categories.ts | CATEGORY namespace constants | | src/types.ts | LogLevel (LogTape severities + "silent") | | src/index.ts | Public barrel exports |