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

@svayam-opensource/svm-util-log

v1.1.0

Published

Svayam's shared logging framework — the logger from svm-util, with only the dependencies a logger needs.

Readme

@svayam-opensource/svm-util-log

Structured logging for Node applications and command-line tools.

Every line carries where it came from, so a log you receive from someone else can be read back to the code that wrote it. Debug output is switched on from the environment, so the person in front of the problem can turn it on without a rebuild. Secrets are masked before anything is written.

npm install @svayam-opensource/svm-util-log

Node 20+. TypeScript types included. Works from both import and require.


Quick start

import { createApplicationLogger, LogLevel } from "@svayam-opensource/svm-util-log";

const logger = createApplicationLogger({
  appId: "gov-work",
  logDir: "/Users/me/.gov/logs",
  methods: [
    { METHOD: "console", LOGLEVEL: LogLevel.warn },   // on screen: warnings and errors
    { METHOD: "file",    LOGLEVEL: LogLevel.debug },  // on disk: everything
  ],
});

logger.log(LogLevel.info, "starting up", "myapp:cli:main", "start");
logger.log(LogLevel.error, "could not reach the registry", "myapp:net:client", "fetch", 0, { status: 503 });

logger.close();   // flush before the process exits

That is the whole setup. Everything below is detail you can come back for.


What a line looks like

2026-09-09T03:41:02.240Z warn    gov-work:cli:agent-install    signIn            7    no browser on this machine — offering the key route
2026-09-09T03:41:02.367Z debug   gov-work:cli:sign-in-choice   captureAgentKey   7    chose the api-key route meta={ user: 'rk', apiKey: '***' }
2026-09-09T03:41:02.494Z error   gov-work:cli:agent-install    run               7    npm could not install the agent meta={ code: 127 }

Columns are fixed-width and the origin comes early, because the usual thing to do with a log is scan one column for one module, not read it as prose.

| Column | From | What it is for | |---|---|---| | 2026-09-09T03:41:02.240Z | automatic | ISO timestamp, UTC | | warn | lvl | severity — see Levels | | gov-work:cli:agent-install | pgm_cd | the code path — which module wrote this | | signIn | fn_cd | which function inside it | | 7 | act_id | ties one unit of work together across modules | | no browser on this machine… | msg | your message | | meta={ … } | meta | structured detail, masked |

Naming pgm_cd — the one convention worth following

Use the code path: <package>:<directory>:<module>.

gov-work:cli:sign-in-choice     →     src/cli/sign-in-choice.ts
myapp:net:client                →     src/net/client.ts

A reader who receives your log can then open the file without guessing, and — as the next section shows — can switch that exact module on by name.


Turning on debug output in the field

Errors and warnings are always written. Everything below them is off until someone asks for it, through the standard Node NODE_DEBUG variable:

NODE_DEBUG=gov-work:cli:sign-in-choice   gov work    # one module
NODE_DEBUG=gov-work:cli:*                gov work    # one directory
NODE_DEBUG=gov-work:*                    gov work    # the whole application

This matters because the person who needs the log is usually not the person who can reproduce the fault. You send them one environment variable; they reproduce it and send back the file. No debug build, no release, no code change.

flowchart TD
    A["logger.log(level, msg, pgm_cd, …)"] --> B{"level is<br/>error or warn?"}
    B -- yes --> D["write it"]
    B -- no --> C{"NODE_DEBUG matches<br/>pgm_cd, fn_cd or act_id?"}
    C -- yes --> D
    C -- no --> E["dropped, costs nothing"]
    D --> F{"for each configured<br/>transport"}
    F --> G["level within<br/>this transport's LOGLEVEL?"]
    G -- yes --> H["mask meta → format → write"]
    G -- no --> E

Two gates, and they are different questions. NODE_DEBUG decides whether this line exists at all; a transport's LOGLEVEL decides where an existing line goes. That is how the quick-start config keeps the screen quiet at warn while the file keeps everything.


Levels

Ordered least to most verbose. A transport set to a level accepts that level and everything above it.

| Level | Use it for | |---|---| | error | the operation failed — always written | | warn | something is wrong but the run continues — always written | | info | milestones a user would recognise | | http | requests and responses | | verbose | more than info, less than a trace | | debug | what you would want during an incident | | silly | everything |

import { withinLevel, LogLevel } from "@svayam-opensource/svm-util-log";

withinLevel(LogLevel.error, LogLevel.info);   // true  — error passes an info threshold
withinLevel(LogLevel.debug, LogLevel.info);   // false — debug does not

Keeping secrets out of logs

A log is a file. It gets copied and sent to other people — precisely when something has gone wrong. So masking happens in the library, before rendering, not in your memory at each call site.

logger.log(LogLevel.debug, "signing in", "myapp:auth:login", "run", 0, {
  user: "rk",
  apiKey: "sk-live-9f3a",
  db: { host: "localhost", password: "hunter2" },
});
… signing in meta={ user: 'rk', apiKey: '***', db: { host: 'localhost', password: '***' } }

Matching is by field name, case-insensitive, at any depth, and matches names that contain a masked word — so userPassword, db_pwd and authToken are all covered.

Covered out of the box:

password  passwd  pwd  secret  token  apiKey  api_key  authorization
auth  credential  credentials  privateKey  private_key  sessionId
session_id  cookie  jwt  bearer

Extend the default, never replace it:

import { DEFAULT_MASK } from "@svayam-opensource/svm-util-log";

createApplicationLogger({
  appId: "myapp",
  logDir: "/var/log/myapp",
  methods: [{ METHOD: "file", LOGLEVEL: LogLevel.info }],
  mask: [...DEFAULT_MASK, "panNumber", "aadhaar"],
});

Passing mask: ["panNumber"] on its own would silently switch off every default — the one mistake worth watching for here.

Masking protects values inside meta. It cannot see a secret you interpolated into the message string yourself, so put values in meta, not in msg.


Where lines go

Two transports ship in this package.

console

Writes to stderr, coloured by level. stderr because stdout is your program's answer — a caller running myapp | jq should not receive log lines as data.

{ METHOD: "console", LOGLEVEL: LogLevel.warn }

file

A daily-rotating file, <date>-<appId>.log, in logDir.

{
  METHOD: "file",
  LOGLEVEL: LogLevel.debug,
  DATA_STORE: {
    dirname: "/var/log/myapp",   // defaults to logDir
    datePattern: "YYYY-MM-DD",
    zippedArchive: true,
    maxSize: "20m",
    maxFiles: "14d",             // older files are deleted
  },
}

Defaults are 20 MB per file, gzipped, kept a fortnight — long enough to diagnose something reported today, short enough that a forgotten log directory is not a growing disclosure risk.

Adding your own

database and api transports are not in this package — they would drag a database driver and an HTTP client into every install. Register one instead:

import { registerTransport } from "@svayam-opensource/svm-util-log";
import Transport from "winston-transport";

class DbTransport extends Transport { /* … */ }

registerTransport("database", (conf, config) => new DbTransport(conf, config));

Register before you call createApplicationLogger. A method named in your config with nothing registered for it is ignored, not an error — the same config file can move between a service that has a database and a CLI that does not, and the CLI simply writes fewer places.

flowchart LR
    L["logger.log(…)"] --> M["mask meta"] --> F["format one line"]
    F --> C["console → stderr"]
    F --> D["file → daily rotate"]
    F --> X["your transport<br/><i>registerTransport</i>"]
    style X stroke-dasharray: 4 3

Configuration reference

createApplicationLogger({
  appId,      // string   — names the app in every line and in the filename
  logDir,     // string   — where file transports write by default
  methods,    // array    — one entry per destination
  mask,       // string[] — optional; extend DEFAULT_MASK
  inspect,    // optional — { depth?: number; maxStringLength?: number } for meta rendering
});

| Field | Required | Notes | |---|---|---| | appId | yes | appears as app_cd and in the log filename | | logDir | yes | must exist or be creatable; used when a file method sets no dirname | | methods | yes | must name at least one destination — see the note below | | mask | no | defaults to DEFAULT_MASK; extend it, don't replace it | | inspect | no | defaults to depth 4, 512-character strings |

Give methods at least one entry. An empty array does not throw, but it is not silence either: winston then prints its own Attempt to write logs with no transports warning for every line, with the entry as raw JSON. That output bypasses this library's formatting — the masking still applies, because meta is masked on the way in, but the result is noisy and not what you configured. logger.toLogObject().ENABLED_TRANSPORTS shows what is really attached.

Configuration is passed in, never read from a global. Nothing has to be initialised before you can log, which matters most on a program's first run — the moment you most want a log and are least likely to have configuration.


API

| Export | What it does | |---|---| | createApplicationLogger(config) | returns an ApplicationLogger | | registerTransport(method, factory) | adds a destination this package does not ship | | LogLevel, LogLevelIndex, logLevelIndex | the level names, their order, their index | | LogMethod | console · file · database · api | | withinLevel(level, threshold) | is level within threshold? | | DEFAULT_MASK | the field names masked by default | | formatLogMessage(entry, config?) | render one line — useful in your own transport | | toLogEntry(appId, lvl, msg, …) | build the entry a transport receives | | maskValues(value, mask?) | mask an object without logging it | | stringify(value, config?) | the meta= renderer; never throws, handles cycles | | isLogEnabled(pgm_cd, fn_cd?, act_id?) | is NODE_DEBUG on for this origin? |

ApplicationLogger

logger.log(level, message, pgm_cd?, fn_cd?, act_id?, meta?);   // write a line
logger.toLogObject();   // what is actually enabled — for a `doctor`/`status` command
logger.toString();      // the same, as JSON
logger.close();         // flush and close every transport

Always close() before a short-lived process exits. File writes are asynchronous, so without it the last lines of a run — the ones explaining why it ended — are the ones most likely to be lost.


Recipes

A command-line tool

Quiet by default, everything on disk, and the user can raise the screen level themselves.

const logger = createApplicationLogger({
  appId: "mycli",
  logDir: path.join(os.homedir(), ".mycli", "logs"),
  methods: [
    { METHOD: "console", LOGLEVEL: LogLevel.warn },
    { METHOD: "file",    LOGLEVEL: LogLevel.debug },
  ],
});

process.on("exit", () => logger.close());

A long-running service

const logger = createApplicationLogger({
  appId: "orders-api",
  logDir: "/var/log/orders-api",
  methods: [
    { METHOD: "console",  LOGLEVEL: LogLevel.info },
    { METHOD: "file",     LOGLEVEL: LogLevel.http },
    { METHOD: "database", LOGLEVEL: LogLevel.error },   // ignored unless registered
  ],
});

Following one request across modules

Give every line of one unit of work the same act_id:

const actId = nextRequestId();
logger.log(LogLevel.http,  "POST /orders", "orders:api:routes", "create", actId);
logger.log(LogLevel.debug, "reserving stock", "orders:domain:stock", "reserve", actId);
logger.log(LogLevel.error, "payment declined", "orders:domain:pay", "charge", actId, { code: "card_declined" });

Then grep ' 4471 ' returns that request and nothing else. NODE_DEBUG accepts an act_id too, so a single transaction can be traced without switching on a whole module.


Troubleshooting

| What you see | Why | |---|---| | Nothing below warn appears | Expected. Set NODE_DEBUG — see above. | | Log lines mixed into piped output | console writes to stderr by design. Redirect with 2>/dev/null, or read it with 2>&1. | | No file appears | Check logDir exists and is writable, and that a file method is in methods. | | The last lines of a run are missing | Call logger.close() before exit. | | A field you expected to be masked is not | Masking reads field names in meta. A secret placed in the message string is not visible to it. | | Your custom transport never writes | registerTransport must run before createApplicationLogger. | | Attempt to write logs with no transports | methods is empty, or every entry names a transport nothing registered. Check logger.toLogObject(). | | mask seems to have switched off the defaults | It did. Use [...DEFAULT_MASK, "yours"]. |


Notes

Console output uses winston; file rotation uses winston-daily-rotate-file.

Design rationale

Why this package is separate from @svayam/svm-util, why configuration is injected rather than global, why console output goes to stderr, and what belongs in this package versus a sibling — all of that is in docs/architecture.md.

That file lives in the source repository, which is private to Svayam; the link below will not open for you unless you have access. Everything in this README stands on its own, and nothing in the architecture document changes how the library behaves.

Svayam engineers: docs/architecture.md.

Licence and support

Copyright © 2026 Svayam Infoware Pvt. Ltd. Published publicly so that Svayam's own public tools — and anyone installing them — can resolve it without registry configuration.