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

pino-quiet

v1.0.1

Published

A lightweight pino transport that collapses repeated consecutive log messages.

Readme

pino-quiet is a lightweight Pino transport that reduces log noise by collapsing repeated consecutive log messages into a single entry with a repetition counter.

It improves readability, cuts storage/ingestion costs, and requires zero changes to your existing logging calls.

{"level":30,"msg":"Connecting..."}     ┐
{"level":30,"msg":"Connecting..."}     ├──▶  {"level":30,"msg":"Connecting... (x3)","repeats":3}
{"level":30,"msg":"Connecting..."}     ┘
{"level":30,"msg":"Connected!"}        ──▶  {"level":30,"msg":"Connected!"}

Features

  • Zero-config — works out of the box with sane defaults.
  • Noise reduction — turns 1,000 "Connection failed" logs into one (x1000) log.
  • Two comparison modes — fast message-only matching (simple, default), or deep object comparison (strict).
  • Composable — can sit before pino-pretty (or any transport) in a pipeline, or run standalone as a terminal transport.
  • Self-healing buffers — an idle repeated log auto-flushes after flushIntervalMs instead of hiding a stale count forever.
  • Hot-loop safemaxRepeats caps how long a single batch can grow before force-flushing.
  • Level-aware matching — optionally never collapse an info and an error that happen to share text.
  • Pluggable equality — bring your own comparator for full control over what counts as "the same log".
  • Observability hookonFlush lets you feed collapse events into your own metrics.
  • Fully typed — first-class TypeScript definitions ship with the package.

Installation

npm i pino-quiet
pnpm add pino-quiet
yarn add pino-quiet

Requires Node.js >= 18 and pino ^10.

Quick start

Standalone (writes to stdout / a file — classic mode)

import pino from 'pino';

const logger = pino({
  transport: {
    target: 'pino-quiet',
    // options are entirely optional — sensible defaults are used otherwise
  },
});

logger.info('Connecting...');
logger.info('Connecting...');
logger.info('Connecting...');
logger.info('Connected!');

// Output:
// {"level":30,"time":...,"msg":"Connecting... (x3)","repeats":3}
// {"level":30,"time":...,"msg":"Connected!"}

Strict mode (deep object comparison)

If you log metadata objects, you might want to deduplicate based on the entire object content (ignoring time, pid, and hostname).

const logger = pino({
  transport: {
    target: 'pino-quiet',
    options: {
      strict: true, // Enable deep comparison
    },
  },
});

// These collapse: the { id: 1 } payload matches even though timestamps differ.
logger.error({ id: 1 }, 'Transaction failed');
logger.error({ id: 1 }, 'Transaction failed');

Options

| Option | Type | Default | Description | | ----------------------- | --------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------- | | destination | string \| number | 1 (stdout) | Destination fd or file path. Ignored when pipeline: true. | | strict | boolean | false | Compare the whole log object (minus ignoreKeys) instead of just msg. | | countField | string | 'repeats' | Field name used to store the total occurrence count. | | ignoreKeys | string[] | [] (merged with built-ins) | Extra fields excluded from strict comparisons. Merged with time/pid/hostname, never replaces them. | | comparator | (prev, curr) => boolean | — | Custom equality function; overrides strict/simple logic entirely when provided. | | levelAware | boolean | false | If true, logs at different levels are never treated as duplicates. | | caseInsensitive | boolean | false | Case-insensitive msg comparison (simple mode only). | | maxRepeats | number | Infinity | Force-flush after this many repeats, even without a new/different log arriving. | | flushIntervalMs | number | 5000 | Auto-flush a buffered log after this many ms of silence. 0 disables this (matches pre-1.0 behavior). | | minRepeatsToAnnotate | number | 1 | Only annotate msg/countField once the repeat count reaches this value. | | annotateMessage | boolean | true | If false, only countField is set; msg is left untouched. | | suffixFormat | (count: number) => string | (count) => ` (x${count})` | Customize the suffix appended to msg. | | includeTimestamps | boolean | false | Add first/last-seen epoch-ms timestamps to every flushed log. | | firstSeenField | string | 'firstSeen' | Field name for the first-seen timestamp (requires includeTimestamps). | | lastSeenField | string | 'lastSeen' | Field name for the last-seen timestamp (requires includeTimestamps). | | onFlush | (log, meta) => void | — | Called on every flush with the emitted log and { repeats, firstSeen, lastSeen }. Never throws. | | pipeline | boolean | false | Pass-through Transform mode for composing with other transports (see below). | | mkdir | boolean | false | Passed through to SonicBoom (destination mode only). | | append | boolean | true | Passed through to SonicBoom (destination mode only). | | sync | boolean | false | Passed through to SonicBoom (destination mode only). |

How it works

  • pino-quiet buffers the most recently seen log.
  • When a new log arrives, it's compared against the buffered one using comparator (if given), otherwise strict/simple matching.
  • Match: the counter increments; nothing is written yet.
  • No match (or the buffered log has sat idle past flushIntervalMs, or hit maxRepeats): the buffered log is flushed (annotated with its final count) and the new log takes its place.
  • On stream shutdown, whatever is still buffered is flushed so no log is ever lost.

Composing with other transports

By default (pipeline not set), pino-quiet is a terminal transport: it owns its destination and writes directly to it, exactly like in the Quick start examples above. That's the simplest setup and is unchanged from 0.9.x.

To place pino-quiet before another transport — most commonly pino-pretty — set pipeline: true. This turns it into a plain pass-through stream that emits deduplicated NDJSON for the next stage to consume, instead of writing anywhere itself:

import pino from 'pino';

const logger = pino({
  transport: {
    pipeline: [
      {
        target: 'pino-quiet',
        options: { pipeline: true, flushIntervalMs: 2000 },
      },
      {
        target: 'pino-pretty',
        options: { colorize: true },
      },
    ],
  },
});

logger.info('Database connection failed');
logger.info('Database connection failed');
logger.info('Database connection failed');
logger.error('Giving up on database. Exiting.');
[12:00:01] INFO: Database connection failed (x3)
    repeats: 3
[12:00:01] ERROR: Giving up on database. Exiting.

This works with any transport that can be a pipeline stage, not just pino-pretty — for example shipping the collapsed stream on to a log-forwarding transport. See example/pipeline-with-pino-pretty.ts for a runnable copy of this example.

Note: destination, mkdir, append, and sync are ignored in pipeline mode — the last stage in the pipeline is responsible for the actual destination.

Migrating from 0.9.x

  1. The stray ] bug is fixed. If you had a workaround stripping a trailing ] from pino-quiet's output, remove it.
  2. flushIntervalMs now defaults to 5000, not disabled. If your tests or tooling depend on the exact pre-1.0 timing (never auto-flush), set flushIntervalMs: 0.
  3. Everything else is additive — no other option names or defaults changed.

Recipes

options: { levelAware: true }
options: { strict: true, ignoreKeys: ['requestId', 'traceId'] }
options: {
  onFlush: (log, meta) => {
    metrics.histogram('log.repeats', meta.repeats);
  },
}
options: { maxRepeats: 500, flushIntervalMs: 1000 }

Contributing

Contributions welcome! Please open an issue for feature requests or bugs before submitting a PR.

git clone https://github.com/Silent-Watcher/pino-quiet.git
cd pino-quiet
npm install
npm test

See CHANGELOG.md for release history.


License

MIT — see LICENSE for details.