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

syntropylog

v2.1.0

Published

Observability framework for Node.js powered by a native Rust engine: declarative correlation, PII masking, per-level field control and retention enforced on every log, serialized + masked + sanitized in one native pass (transparent JS fallback). Failsafe

Readme


At a glance

| | | |---|---| | Runtime | Node.js >= 22 · ESM + CJS · TypeScript types included | | Install | npm install syntropylog — zero runtime dependencies. The Rust addon is an optional dependency; without it the pure-JS pipeline runs, with identical output | | HTTP frameworks | Express (correlationIdMiddleware), Fastify (fastifyCorrelationHook), NestJS (syntropylog/nestjs). For anything else — Koa, Hono, a bare server — the resolver is framework-agnostic | | Destinations | Any. You write a one-function executor; the framework stays independent of client-library versions | | Propagation | One correlation-id across processes. Per-target wire-name maps (outbound: { http, kafka, … }) + getPropagationHeaders(target), so services need not agree on header names | | Interop | Composes with OpenTelemetry: W3C traceparent understood on the way in, OTLP export on the way out | | Testing | syntropylog/testing — SpyTransport, mocks and helpers, so assertions run without a live backend | | Self-observability | getStats() — failure counters, fallbacks, uptime, native-addon state | | Bundle | sideEffects: false, tree-shakeable | | Other runtimes | Same model, built idiomatically per platform: .NET → sl4n on Microsoft.Extensions.Logging · Python → slpy on contextvars/asyncio, with its own optional Rust masking engine | | License | Apache-2.0 |


Install

npm install syntropylog
import { syntropyLog } from 'syntropylog';

// Configure once — this is all you need.
await syntropyLog.init({ logger: { serviceName: 'payments' } });

// Log an object. Sensitive fields are masked before any transport sees them.
syntropyLog.getLogger().info({ email: '[email protected]', password: 'hunter2' }, 'payment ok');
{"email":"r***@x.com","level":"info","message":"payment ok","password":"[REDACTED]","service":"payments","timestamp":"2026-06-14T13:11:48.060+00:00"}

That is default behavior, not magic — and it is identical under the native Rust engine and the pure-JS fallback. Everything below is what you can then declare: which fields each level emits, where logs go, how context crosses services, what retention class an audit record carries.

Masking is by field name. A field whose key matches a rule is masked. Anything without a known key — array elements, or text you concatenate into the message — passes through untouched. Pass sensitive data as keyed fields (log.info({ email }, 'msg')), not as message text. Masking enforces your rules on keyed fields; it cannot find PII you hide in prose.

New in this release: CHANGELOG.md. Bigger picture and a full example: docs/getting-started.md.


Where it sits next to Pino, Winston and OpenTelemetry

Pino and Winston are fast loggers; OpenTelemetry is the transport and instrumentation standard for telemetry. Neither governs the content of a log — which fields get masked, what context each level may emit, what retention class an audit event carries, how correlation crosses HTTP and a broker. That governance layer is what SyntropyLog is, and it composes with the other two: its logs can flow out through OTel, and the correlation middleware understands traceparent.

Choose Pino + OTel alone when PII, audit and retention are thin requirements you can afford to hand-roll. Choose SyntropyLog when they are first-class — and keep OTel for traces either way.

Feature-by-feature comparison, benchmark method and a migration path: docs/migration-from-pino.md · docs/opentelemetry-integration.md



Five things a logger doesn't do

This is where SyntropyLog earns its place. Each one is a mechanism, not a feature bullet — and each carries a subtlety that only shows up in production.

1. One correlation-id that survives a broker and a language boundary

Inside a process, context rides AsyncLocalStorage, so it reaches every log in scope without being threaded through function signatures. Across processes it travels through wire-name maps you declare per target:

// { contextField: wireName } per target — the same id, named per the convention of each hop
context: {
  outbound: {
    http:  { correlationId: 'x-correlation-id', tenantId: 'x-tenant-id' },
    kafka: { correlationId: 'correlationId',    tenantId: 'tenantId'    },
  },
}

contextManager.getPropagationHeaders('kafka')  // → { correlationId: '…', tenantId: '…' }

The point is that services do not have to agree on header names. Each declares its own, and the maps translate — which is what makes the id survive a hop into a system you do not control. Inbound, correlationIdMiddleware / fastifyCorrelationHook accept a configurable list of headers plus W3C traceparent.

Example 22-distributed-orders-kafka threads one id through Express → NestJS → Kafka → a Python service → a worker: seven services, a message broker and a language boundary, with the card number and CVV masked the whole way. → context.md

2. Field control that is a whitelist, not a convention

The Logging Matrix declares, per level, which context fields may be emitted. A field absent from that level's list never reaches a transport — it is not filtered downstream, it is not there. error can take ['*'] while info takes ['correlationId'], so a debug field cannot leak into production logs because somebody forgot to remove it. → logging-matrix.md

3. Masking that one sink is allowed to opt out of

Masking runs once, before the transport loop, so every sink receives the same obfuscated entry. That is right for consoles and APMs and wrong for exactly one: the audit journal, where 2*****9 proves nothing. masking.exemptTransports names the sinks that get the truth — declared in your config, never by a transport about itself, and an unknown name fails loud at init() rather than silently masking the one sink that had to hold evidence. With the native engine on, the masked and the unmasked rendering come out of a single parse (fastSerializeFromJsonDual), so an app with no exempt transport pays nothing for the feature existing.

Two things worth knowing before writing a rule: masking is keyed on the field name, and masking.regexTimeoutMs is inert — V8 cannot interrupt a running regex, so explosive patterns are rejected statically at init() instead of being timed out at runtime. The library says so instead of implying a guarantee it cannot keep. → masking.md

4. Retention as a bridge, not a payload

Retention is decided per record — only the application can tell a payment authorization from a health check — and enforced per container: an index, a stream, a bucket. Those two facts live in different places, and the log entry is the bridge.

withRetention('OPERACIONES') puts the class name on the entry: always a string, because every mechanism downstream (a Loki label matcher, a Datadog index filter, sink routing) matches on a low-cardinality string, and a field that is a string on some entries and an object on others is a mapping conflict at ingest. Alongside it travels retentionUntil, the end of the mandatory window, materialized so a sweep is a range scan instead of a policy interpretation.

An in-process consumer needs nothing else: it resolves the name at write time with getRetentionPolicy(name) against the same frozen registry (getRetentionPolicies() lists it). A consumer out of process — a shipper reading JSON, with no registry to resolve against — can have the full rules travel on the entry with retention: { emitRules: true, version: 'E6-1' }; off by default, and the version stamp is what lets a persisted record say which revision it was filed under.

The details are the point: retentionUntil is not an expiry — reaching it ends the obligation, it does not authorize deletion. A leap-day record lands on 1-Mar, kept one day longer, never one day short, because ending a window early is the failure an auditor punishes. And a policy without whole years gets null rather than a guessed date in a compliance column. → compliance.md · DESIGN-retention-bridge.md

5. Logging that cannot take your process down

A transport that throws, a serializer that exceeds its timeout, a circular reference, a native addon that fails to load — none of them reach your call site. Failures surface through hooks (onLogFailure, onTransportError, onStepError, onSerializationFallback) and through getStats() counters, so a silent degradation is still observable.

For audit entries that must not be lost, DurableAdapterTransport adds buffer, exponential-backoff retry and a dead-letter queue — and with an opt-in persistPath, survives process restarts.

The Rust addon belongs to this same contract by not being part of it: it is a performance optimization, never a behavioral one. Output is identical to the JS pipeline, which is what runs on any platform without a prebuilt binary. → lifecycle.md · native-addon.md


What SyntropyLog is not

It is a structured-logging and context-propagation framework. It is not a log aggregation backend (use Elasticsearch / Loki / CloudWatch), a distributed-tracing system (use OpenTelemetry — see the integration guide), or a metrics collector (use Prometheus / Datadog). It is the component that makes every log line correct, consistent, and safe before it reaches any of those systems.


Security & supply chain

  • No network I/O at runtime. The framework contacts no external URLs; the only output is what your transports produce.
  • Zero runtime dependencies (dependencies: {}). The optional native addon is built from auditable Rust source in the same repo — no opaque prebuilt binaries; transparent JS fallback.
  • No environment sniffing — configuration is passed to init(); the package reads no env vars on its own.
  • Hardened pipeline: prototype-pollution guard (__proto__/constructor/prototype stripped at every depth), ReDoS-safe masking (explosive patterns rejected at init — no runtime timeout is possible in JS, so none is claimed), Silent Observer (logging never throws).
  • Supply chain: all devDeps pinned to exact versions, pnpm.overrides verified, NPM provenance signing on publish; pnpm audit reports 0 vulnerabilities.

Full details: SECURITY.md.


What's in the box

| Feature | One-liner | Docs | |---|---|---| | Logging Matrix | Whitelist of context fields per level; defineMatrix() for typed keys | logging-matrix.md | | MaskingEngine | Redact PII before transport; getDefaultMaskingRules, maskEnum, ReDoS-safe; exemptTransports gives one sink the unmasked truth | masking.md | | Universal Adapter | One executor → any backend; framework stays agnostic | transports.md | | DurableAdapterTransport | Buffer + backoff retry + DLQ; delivery guarantees for retention-tagged audit entries; opt-in persistPath disk spool survives restarts | compliance.md | | Transport pool & per-env routing | transportList + env; per-call override/add/remove | transports.md | | Fluent API | child, withSource, withTransactionId, withMeta, withRetention; defineRetentionPolicies() registry | fluent-api.md | | Context propagation | Correlation + transaction IDs via AsyncLocalStorage; inbound/outbound wire-name translation | context.md | | Express / Fastify | correlationIdMiddleware() / fastifyCorrelationHook() — multi-header + W3C traceparent + response echo | context.md | | NestJS module | syntropylog/nestjs: SyntropyLogModule, SyntropyNestLoggerService, @InjectLogger() | nestjs.md | | Retention bridge | Always-on audit level; withRetention('NAME') puts the class name + retentionUntil on the entry; getRetentionPolicy / getRetentionUntil for write paths without a logger | DESIGN-retention-bridge.md | | Lifecycle, hooks & serialization | init/shutdown, onLogFailure, timeout/depth limits, circular-ref immunity | lifecycle.md | | Self-observability | getStats() — failure counters, fallbacks, uptime, native-addon state | lifecycle.md | | Testing toolkit | syntropylog/testing: SpyTransport, createTestHelper, createServiceWithMock | testing-mocks.md | | Multi-instance factory | createSyntropyLog() returns independent instances | lifecycle.md | | Runtime reconfiguration | Hot-change level / matrix / debug transport | runtime-reconfiguration.md | | Native addon (Rust) | Single-pass serialize + mask + sanitize; transparent JS fallback | native-addon.md | | OpenTelemetry export | Emit to an OTLP collector via UniversalAdapter | opentelemetry-integration.md | | Prototype-pollution defense | __proto__/constructor/prototype stripped at the pipeline boundary | compliance.md | | Tree-shaking | sideEffects: false + ESM | — |


Documentation & examples

cd 00-setup-initialization && npm install && npm run dev

Contributing & License

See CONTRIBUTING.md and SECURITY.md. License: Apache-2.0.