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

@provide-io/telemetry

v0.11.0

Published

Structured logging, OTEL traces/metrics for TypeScript (Node.js and browsers) — parity with provide.telemetry Python package

Downloads

759

Readme

@provide-io/telemetry

Structured logging + OpenTelemetry traces and metrics for TypeScript — feature parity with the provide-telemetry Python package.

Install

npm install @provide-io/telemetry

Optional OTEL peer dependencies

To export traces, metrics, and logs to an OTLP endpoint (e.g. OpenObserve, Jaeger, Tempo):

npm install \
  @opentelemetry/sdk-trace-base \
  @opentelemetry/sdk-metrics \
  @opentelemetry/sdk-logs \
  @opentelemetry/resources \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/exporter-logs-otlp-http \
  @opentelemetry/api-logs \
  @opentelemetry/context-async-hooks

All nine are optional — the library degrades gracefully to no-op providers when they are absent.

@opentelemetry/context-async-hooks is the one worth calling out. On Node it provides the AsyncLocalStorage context manager that startActiveSpan needs to carry a span across an await boundary. Without it telemetry still exports, but child spans are emitted with no parent, so the trace tree is what degrades — which reads as a tracing bug rather than a missing dependency. When it is absent on Node, getHealthSnapshot().setupError and getRuntimeStatus().setupError both say so. Browser and edge builds have no AsyncLocalStorage and are not meant to; there the library stays silent.

Quick start

import { setupTelemetry, getConfig, getLogger, registerOtelProviders, shutdownTelemetry } from '@provide-io/telemetry';

// Call once at app startup.
setupTelemetry({
  serviceName: 'my-app',
  environment: 'production',
  version: '1.0.0',
  logLevel: 'info',
  logFormat: 'json',
  otelEnabled: true,
  tracingEnabled: true,
  otlpEndpoint: 'http://localhost:4318',
  otlpHeaders: { Authorization: 'Basic ...' },
});

// Activate OTLP exporters (requires peer deps above).
await registerOtelProviders(getConfig());

const log = getLogger('api');
log.info({ event: 'request.received.ok', method: 'GET', path: '/health', status: 200 });

// On shutdown — flushes and drains all OTel providers.
await shutdownTelemetry();

API reference

Setup

| Export | Description | |--------|-------------| | setupTelemetry(config) | Configure the library. Idempotent — safe to call multiple times. | | getConfig() | Return the current TelemetryConfig. | | configFromEnv() | Build config from environment variables (see Configuration). | | registerOtelProviders(cfg) | Wire OTLP log, trace, and metric exporters for the signals enabled in config. Call after setupTelemetry. | | flushTelemetry(timeoutMs?) | Drain every registered provider and leave them installed. Resolves false if any missed the deadline. | | shutdownTelemetry() | Flush and shut down all registered OTel providers. |

Logging

import { getLogger } from '@provide-io/telemetry';

const log = getLogger('my-module');
log.debug({ event: 'cache.miss.ok', key: 'user:42' });
log.info({ event: 'request.complete.ok', status: 200, duration_ms: 14 });
log.warn({ event: 'retry.attempt.warn', attempt: 2 });
log.error({ event: 'db.query.error', error: err.message });

Event names follow the DA(R)S pattern: 3 segments (domain.action.status) or 4 segments (domain.action.resource.status).

The SDK stamps service, env and version from config and, while a span is live, trace_id and span_id on every record. A field you log or bind under one of those names is replaced by the stamped value; to name a remote service, use peer.service.

Callsite capture

Two independent knobs select two different outputs from one stack walk. Enabling either one performs the walk; neither implies the other.

| Knob | Output | Keys | |------|--------|------| | logIncludeCaller / PROVIDE_LOG_INCLUDE_CALLER | Fields on the log record | filename, lineno | | logCodeAttributes / PROVIDE_LOG_CODE_ATTRIBUTES | Attributes on the exported OTLP log record | code.file.path, code.line.number, code.function.name |

filename carries the base name of the caller's source file, never a full path — an absolute path leaks the build machine's layout into every record. code.file.path carries the whole path, which is what OpenTelemetry defines that attribute as and what the other SDKs send. lineno and code.line.number are 1-based.

The reported frame is the caller's. The walk identifies this module's own frames by the file the capture itself is in, so it holds for the source tree, the published build, and a consumer whose own module happens to be called logger.ts; pino's frames are matched by path segment under node_modules.

Bundling the SDK and the application into one file leaves nothing to tell those apart, and there the record carries no callsite at all rather than one naming the logger's own line.

code.function.name carries the name of the enclosing function. It is omitted when the runtime resolves no name for the frame — top-level module code and anonymous callbacks produce nameless stack frames, and an absent attribute is more useful than one whose value means "unknown".

Tracing

import { withTrace, getActiveTraceIds, setTraceContext } from '@provide-io/telemetry';

const result = await withTrace('my.operation.ok', async () => {
  const { trace_id, span_id } = getActiveTraceIds();
  // trace_id / span_id are available here and in any log emitted inside
  return doWork();
});

Metrics

import { counter, gauge, histogram, observableGauge } from '@provide-io/telemetry';

const requests = counter('http.requests', { unit: '1', description: 'Total HTTP requests' });
requests.add(1, { method: 'GET', status: '200' });

const latency = histogram('http.duration', { unit: 'ms' });
latency.record(42, { route: '/api/users' });

const cpu = gauge('cpu.utilization', { unit: '%' }); // an OTel Gauge: last value wins
cpu.set(72.5);

// Values computed at collection time (each scrape or export interval).
const depth = observableGauge('queue.depth', () => [{ value: queue.length }]);
depth.unregister();

Prometheus (Node only)

With OTEL_METRICS_EXPORTER=prometheus (or otlp,prometheus), registerOtelProviders() attaches a Prometheus pull reader. Mount the handler on a server you already run, or set PROVIDE_METRICS_PROMETHEUS_LISTEN=true for a standalone listener on 127.0.0.1:9464/metrics:

import { metricsHandler, renderPrometheus } from '@provide-io/telemetry/prometheus';

app.get('/metrics', metricsHandler()); // node:http, Express, Connect
const { body, contentType } = await renderPrometheus(req.headers.accept);

TLS and basic auth for the listener come from a Prometheus web config file named by PROVIDE_METRICS_PROMETHEUS_WEB_CONFIG, which needs the optional yaml and bcryptjs packages (npm install yaml bcryptjs). Every scrape also carries the SDK's own health as provide_telemetry_* series; PROVIDE_METRICS_HEALTH_OTLP=true exports them over OTLP as well. See "Serving Metrics for Prometheus to Scrape" in docs/guide/configuration.md. The browser build has no pull support.

Context binding

import { bindContext, runWithContext, clearContext } from '@provide-io/telemetry';

bindContext({ request_id: 'req-abc', user_id: 7 });
// All log calls in this async context will include these fields automatically.
clearContext();

// Scoped — context is automatically cleaned up after fn resolves.
await runWithContext({ trace_id: '...' }, async () => { /* ... */ });

Session correlation

import { bindSessionContext, getSessionId, clearSessionContext } from '@provide-io/telemetry';

bindSessionContext('sess-abc-123');
// All logs and traces now include session_id automatically.
const sid = getSessionId(); // 'sess-abc-123'
clearSessionContext();

Error fingerprinting

import { computeErrorFingerprint } from '@provide-io/telemetry';

try {
  throw new Error('connection refused');
} catch (e) {
  const err = e as Error;
  const fp = computeErrorFingerprint(err.constructor.name, err.stack);
  // fp: 12-char hex digest, stable across deploys — use for dedup and alert grouping.
}

W3C trace propagation

import { extractW3cContext, bindPropagationContext } from '@provide-io/telemetry';

// In an HTTP handler — extract incoming traceparent/tracestate/baggage.
// Takes a Record<string, string>; node:http's IncomingHttpHeaders needs narrowing.
const ctx = extractW3cContext(req.headers as Record<string, string>);
bindPropagationContext(ctx);

PII sanitization

import { sanitize, registerPiiRule } from '@provide-io/telemetry';

// Built-in: redacts password, token, secret, authorization, api_key, ...
const obj = { user: 'alice', password: 'hunter2' }; // pragma: allowlist secret
sanitize(obj);
// sanitize() mutates in place — obj is now { user: 'alice', password: '***' }

// Custom rule (dot-separated path; '*' as wildcard segment)
registerPiiRule({ path: 'user.ssn', mode: 'redact' });

sanitize is a package-root export. Import it from @provide-io/telemetry; the package does not publish a separate sanitize subpath.

Health snapshot

import { getHealthSnapshot } from '@provide-io/telemetry';

const snap = getHealthSnapshot();
// snap.exportFailuresLogs, snap.tracesDropped, ...

A log record counts in logsEmitted once it has passed the sinks. One refused before export — by consent, sampling or backpressure — counts in logsDropped instead, never in both.

Runtime inspection

import { getRuntimeConfig, getRuntimeStatus } from '@provide-io/telemetry';

const cfg = getRuntimeConfig();
const status = getRuntimeStatus();

console.log(cfg.serviceName);
console.log(status.setupDone, status.providers.traces, status.fallback.logs);

Use getRuntimeConfig() after setup or runtime reloads to inspect the applied snapshot, and getRuntimeStatus() to see signal enablement, provider install state, fallback mode, and the last setup error without reading internal modules.

Runtime reconfiguration

Use reconfigureTelemetry() to change config at runtime without a process restart. It merges a partial TelemetryConfig over the current one and re-runs setupTelemetry() with the result, rebuilding the logger so fresh calls to getLogger() observe the new config.

import { reconfigureTelemetry, getRuntimeConfig } from '@provide-io/telemetry';

// Rotate to a new OTLP collector endpoint mid-flight.
reconfigureTelemetry({
  otlpEndpoint: 'http://collector.new.internal:4318',
  otlpHeaders: { Authorization: 'Basic ...' },
});

console.log(getRuntimeConfig().otlpEndpoint);
// → 'http://collector.new.internal:4318'

Guardrails:

  • If OTel providers are already registered (i.e. registerOtelProviders() has run) and a provider-changing field differs — serviceName, environment, version, otelEnabled, tracingEnabled, metricsEnabled, any otlp*Endpoint / otlp*Headers — this call throws ConfigurationError. Provider swap requires a process restart to avoid losing buffered exports. For hot-reloadable fields only (sampling, backpressure, exporter resilience, schema strictness, SLO toggles, security limits, PII depth), use updateRuntimeConfig() or reloadRuntimeFromEnv() instead — they never touch provider wiring.

AsyncLocalStorage initialisation (ESM gotcha)

Under Node.js the library uses node:async_hooks.AsyncLocalStorage to isolate propagation context per async task. In CJS builds the store is attached synchronously at module load. Under pure ESM (.mjs entrypoints, tsx --import) require is undefined, so the library falls back to a fire-and-forget await import('node:async_hooks') that resolves on the next microtask. Callers that bind propagation context inside top-level async code before that microtask runs would hit the module-level fallback store and leak context between concurrent requests.

For code paths that need a hard guarantee (typically: servers that start accepting requests at module scope) prefer the async variant:

import { setupTelemetryAsync } from '@provide-io/telemetry';

// Awaits ALS init before resolving. Throws ConfigurationError when
// AsyncLocalStorage is genuinely unavailable on a Node runtime.
await setupTelemetryAsync({ serviceName: 'my-app' });

// Safe to accept concurrent requests here.

The synchronous setupTelemetry() remains the right choice for non-async-init environments (bundled CJS tests, vitest, scripts that do not race with request serving). It applies a best-effort ALS check: when the init has not yet settled it schedules a deferred check that records a setupError and logs a warning if ALS really is unavailable — it does not throw in that case.

If you would rather compose the primitives yourself, both helpers remain exported:

import {
  setupTelemetry,
  awaitPropagationInit,
  isFallbackMode,
} from '@provide-io/telemetry';

setupTelemetry({ serviceName: 'my-app' });
await awaitPropagationInit();
if (isFallbackMode()) {
  throw new Error('AsyncLocalStorage unavailable — refusing to serve requests');
}

awaitPropagationInit() always resolves (it never rejects); inspect isFallbackMode() / isPropagationInitDone() afterwards to branch on the outcome.

React integration

The core package does not require React. Install React 18+ only when importing the React subpath.

import { useTelemetryContext, TelemetryErrorBoundary } from '@provide-io/telemetry/react';

useTelemetryContext(values)

Binds key/value pairs into telemetry context for the lifetime of a component. Automatically cleans up on unmount and re-runs when values change (compared by content, not reference).

function UserDashboard({ userId }: { userId: string }) {
  useTelemetryContext({ user_id: userId, page: 'dashboard' });
  return <Dashboard />;
}

TelemetryErrorBoundary

React error boundary that auto-logs caught render errors via getLogger('react.error_boundary'). Accepts a static fallback or a render-prop receiving the error and a reset callback.

<TelemetryErrorBoundary
  fallback={(error, reset) => <ErrorPage error={error} onRetry={reset} />}
  onError={(error, info) => reportToSentry(error, info)}
>
  <App />
</TelemetryErrorBoundary>

Browser support

The library is browser-compatible via conditional exports. OpenTelemetry providers become no-ops in browser environments, so no server-only imports leak into client bundles. No polyfills are required for modern browsers (ES2020+).

Browser-specific options in setupTelemetry():

| Option | Effect | |--------|--------| | captureToWindow: true | Buffers structured logs to window.__pinoLogs for devtools inspection | | consoleOutput: true | Mirrors log output to console.debug / console.log / console.warn / console.error |

Configuration

All options can be set programmatically via setupTelemetry() or via environment variables:

| Env var | Default | Description | |---------|---------|-------------| | PROVIDE_TELEMETRY_SERVICE_NAME | provide-service | Service identity attached to all signals | | PROVIDE_TELEMETRY_ENV | dev | Deployment environment tag (e.g. dev, staging, prod) | | PROVIDE_TELEMETRY_VERSION | 0.0.0 | Application version tag | | PROVIDE_TELEMETRY_STRICT_SCHEMA | false | Master switch: when true, overrides event name strictness to on | | PROVIDE_TELEMETRY_STRICT_CONFIG | false | When true, a configuration value the SDK would otherwise warn about and ignore fails config parsing with ConfigurationError instead. Today that covers OTEL_METRICS_EXPORTER only (prometheus_metrics.metrics_exporter_warnings); it does not touch event-name strictness (PROVIDE_TELEMETRY_STRICT_SCHEMA) or values that are already errors. | | PROVIDE_LOG_LEVEL | INFO | Log level: TRACE, DEBUG, INFO, WARNING, ERROR, CRITICAL | | PROVIDE_LOG_FORMAT | console | Renderer: console, json, or pretty | | PROVIDE_LOG_INCLUDE_TIMESTAMP | true | Add ISO-8601 timestamp to each log event | | PROVIDE_LOG_INCLUDE_CALLER | true | Add filename and line number to each log event | | PROVIDE_LOG_SANITIZE | true | Enable PII/sensitive field redaction in log output | | PROVIDE_LOG_PII_MAX_DEPTH | 8 | Maximum nesting depth for PII/sensitive field traversal during sanitization | | PROVIDE_LOG_CODE_ATTRIBUTES | false | Attach code attributes to OTel log records | | PROVIDE_LOG_PRETTY_KEY_COLOR | dim | ANSI color name for keys in pretty format (see named colors below) | | PROVIDE_LOG_PRETTY_VALUE_COLOR | "" | ANSI color name for values in pretty format (empty = default) | | PROVIDE_LOG_PRETTY_FIELDS | "" | Comma-separated field names to display in pretty format | | PROVIDE_LOG_MODULE_LEVELS | "" | Per-module log level overrides (e.g. provide.server=DEBUG,asyncio=WARNING) | | PROVIDE_LOG_OTLP_ENABLED | true | Enable OTLP log export (independent of PROVIDE_TRACE_ENABLED / PROVIDE_METRICS_ENABLED; set false to skip OTLP handler even when OTEL_EXPORTER_OTLP_ENDPOINT is set) | | PROVIDE_TRACE_ENABLED | true | Enable OTel tracing provider (falls back to no-op when false) | | PROVIDE_TRACE_SAMPLE_RATE | 1.0 | Trace sampling rate (0.0-1.0) applied to the OTel SDK ParentBased(TraceIdRatioBased) sampler (and facade when no live provider); combined via min() with PROVIDE_SAMPLING_TRACES_RATE | | OTEL_EXPORTER_OTLP_ENDPOINT | "" | Shared OTLP endpoint (fallback for all signals) | | OTEL_EXPORTER_OTLP_HEADERS | "" | Shared OTLP headers (fallback for all signals) | | OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | "" | Per-signal OTLP endpoint for logs | | OTEL_EXPORTER_OTLP_LOGS_HEADERS | "" | Per-signal OTLP headers for logs | | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | "" | Per-signal OTLP endpoint for traces | | OTEL_EXPORTER_OTLP_TRACES_HEADERS | "" | Per-signal OTLP headers for traces | | OTEL_EXPORTER_OTLP_METRICS_ENDPOINT | "" | Per-signal OTLP endpoint for metrics | | OTEL_EXPORTER_OTLP_METRICS_HEADERS | "" | Per-signal OTLP headers for metrics |

OTEL_METRICS_EXPORTER (otlp, prometheus, otlp,prometheus or none) and the Prometheus variables are covered under Prometheus. Any other entry (console, a typo, none mixed with another) does not fail setup: it is dropped, logged as config.metrics_exporter.value_ignored and reported in getHealthSnapshot().setupError, falling back to otlp when nothing usable is left. PROVIDE_TELEMETRY_STRICT_CONFIG=true makes it a ConfigurationError instead. OTLP over TLS reads OTEL_EXPORTER_OTLP_CERTIFICATE, _CLIENT_CERTIFICATE and _CLIENT_KEY, each with per-signal forms that win; a file that cannot be read or loaded leaves that signal's exporter unbuilt, logs otlp.tls_file_unreadable and sets getHealthSnapshot().setupError. The full list is in docs/guide/configuration.md.

Pretty renderer

Set logFormat: 'pretty' (or PROVIDE_LOG_FORMAT=pretty) for human-readable colored output during local development. Color support respects FORCE_COLOR, NO_COLOR, and stderr TTY detection.

setupTelemetry({ logFormat: 'pretty' });

Requirements

  • Node.js ≥ 22
  • TypeScript ≥ 5 (built with TypeScript 6)

License

Apache-2.0. See LICENSE.