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

@0dep/pino-applicationinsights

v2.0.1

Published

Pino applicationinsights transport

Readme

pino applicationinsights transport

BuildCoverage Status

Forward pino logger to Application Insights.

Works with both applicationinsights@2 and applicationinsights@3 (the v3 classic-API shim over @azure/monitor-opentelemetry-exporter). The peer dep range is >=2 <4. See Application Insights v2 vs v3 for behavioural differences and caveats.

Have a look in Example app to get inspiration of how to use this lib.

Ships with fake applicationinsights helper test class.

Usage

import { pino } from 'pino';
import compose from '@0dep/pino-applicationinsights';
import { TelemetryClient } from 'applicationinsights';

// `client.context.keys` works on both v2 and v3 — v2's `Contracts.ContextTagKeys`
// is gone in v3.
const tagKeys = new TelemetryClient(process.env.APPLICATIONINSIGHTS_CONNECTION_STRING).context.keys;

const transport = compose({
  track(chunk) {
    const { time, severity, msg: message, properties, exception } = chunk;
    this.trackTrace({ time, severity, message, properties });
    if (exception) this.trackException({ time, exception, severity });
  },
  connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
  config: { maxBatchSize: 1 },
});

const logger = pino(
  {
    level: 'trace',
    mixin(context) {
      return {
        tagOverrides: {
          [tagKeys.userId]: 'someUserIdPickedFromRequest',
          ...context.tagOverrides,
        },
      };
    },
  },
  transport,
);

Note: tagOverrides is honoured by applicationinsights@2 only. The v3 classic-API shim ignores it — see Application Insights v2 vs v3. For cross-version distributed-trace correlation, pass tracing from the mixin instead — see Distributed tracing.

or as multi transport:

import { pino } from 'pino';

const transport = pino.transport({
  targets: [
    {
      level: 'info',
      target: '@0dep/pino-applicationinsights',
      worker: {
        env: { ...process.env, APPLICATION_INSIGHTS_NO_STATSBEAT: 'disable' },
      },
      options: {
        connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
        config: {
          disableStatsbeat: true,
        },
      },
    },
    {
      level: 'debug',
      target: 'pino-pretty',
      options: {
        colorize: true,
        ignore: 'pid,hostname',
        translateTime: "yyyy-mm-dd'T'HH:MM:ss.l",
      },
    },
  ],
});

const logger = pino({ level: 'trace' }, transport);

Distributed tracing

Correlate pino log records to an OpenTelemetry trace by returning a tracing object from the pino mixin. The default trackTraceAndException forwards it as Application Insights ai.operation.id / ai.operation.parentId on both SDK versions:

  • v2 — auto-merges { [client.context.keys.operationId]: traceId, [client.context.keys.operationParentId]: spanId } into tagOverrides, which v2 copies to the wire envelope's tags map.
  • v3 — wraps the trackTrace / trackException calls in an OpenTelemetry context with the given span context so the @azure/monitor-opentelemetry-exporter stamps operation_Id / operation_ParentId on the wire envelope. This requires @opentelemetry/api to be resolvable at runtime — declared as peer dependency and satisfied transitively by both applicationinsights@2 and applicationinsights@3.
import { pino } from 'pino';
import { trace } from '@opentelemetry/api';
import compose from '@0dep/pino-applicationinsights';

const transport = compose({
  connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
  config: { maxBatchSize: 1 },
});

const logger = pino(
  {
    level: 'trace',
    mixin() {
      const span = trace.getActiveSpan();
      if (!span) return {};
      const { traceId, spanId, traceFlags, traceState } = span.spanContext();
      return { tracing: { traceId, spanId, traceFlags, traceState: traceState?.serialize() } };
    },
  },
  transport,
);

Fields on the tracing object:

  • traceId — 32-hex-char W3C trace id, forwarded as ai.operation.id.
  • spanId — 16-hex-char W3C span id, forwarded as ai.operation.parentId.
  • traceFlags (optional, defaults to 1 / sampled) — honoured on v3 only.
  • traceState (optional) — honoured on v3 only.

Precedence: user-supplied tagOverrides entries always win over the auto-derived correlation ids — set tagOverrides[tagKeys.operationId] explicitly to override.

Worker-thread serialization

When used via pino.transport({ targets: [...] }) the transport runs in a worker thread, so the mixin output is serialised across the worker boundary. tracing must be plain JSON — a live OpenTelemetry Span object cannot cross, which is why the mixin extracts traceId / spanId from span.spanContext() rather than passing the span itself.

Custom track functions

Callers that pass a custom track callback to compose should wrap their trackTrace / trackException calls with the exported applyTracing(chunk.tracing, () => { ... }) helper to preserve correlation across both SDK versions.

import compose, { applyTracing } from '@0dep/pino-applicationinsights';

compose({
  track(chunk) {
    applyTracing(chunk.tracing, () => {
      const { time, severity, msg: message, properties } = chunk;
      this.trackTrace({ time, severity, message, properties });
    });
  },
  connectionString: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING,
});

Graceful shutdown

When the pino source stream closes — either through transport.end() / transport.destroy() or via pino.final / logger.flush() chained off a signal handler — compose flushes and tears down the underlying TelemetryClient so any records buffered by the SDK reach Application Insights before the process exits. On v3 this calls client.shutdown() (which forces the OTel BatchLogRecordProcessor to flush and shuts down the global LoggerProvider). On v2 it calls client.flush() (which drains the channel buffer). Without this hook, records sitting in the v3 batch processor or v2 channel buffer at process exit are silently dropped.

To wire this to OS signals, attach a handler that flushes pino and exits. pino@10 removed the pino.final helper, so call logger.flush(cb) directly — see example/logger.js:

function finalize() {
  logger.flush((err) => process.exit(err ? 1 : 0));
}
process.once('SIGTERM', finalize);
process.once('SIGINT', finalize);

On pino@9 and earlier, the equivalent is pino.final(logger, (err) => process.exit(err ? 1 : 0)).

SIGKILL cannot be intercepted by any process, so records in flight at that moment are unrecoverable — that's a kernel-level constraint, not a transport issue.

API

compose(opts[, TelemetryTransformation]) => Stream

Build transport stream function.

  • opts:
    • connectionString: Application Insights connection string. A bare instrumentation key works on v2 only — v3 requires the full InstrumentationKey=…;IngestionEndpoint=… form.
    • track(chunk): optional track function called with Telemetry client context, defaults to tracking trace and exception (trackTraceAndException)
    • config: optional Application Insights Telemetry client config. disableStatsbeat: true only takes effect on v2; v3 needs the env-var recipe (see statsbeat caveat).
    • destination: optional destination stream, makes compose ignore the above options
    • ignoreKeys: optional pino ignore keys, used to filter telemetry properties, defaults to ['hostname', 'pid', 'level', 'time', 'msg']
  • TelemetryTransformation: optional transformation stream extending TelemetryTransformation

class TelemetryTransformation(options[, config])

Telemetry transformation stream. Transforms pino log record to Telemetry:ish object.

  • constructor(options[, config])
    • options: transform stream options, { objectMode: true } is always set
    • config: optional config object
      • ignoreKeys: optional pino ignore keys as string array
  • _transform(chunk, encoding, callback)
  • convertToTelemetry(chunk): convert pino log record string or object to telemetry:ish object
  • convertLevel(level): map pino log level number to the SDK's severity enum value. The exact value depends on the installed applicationinsights version — numeric Contracts.SeverityLevel (e.g. 1) on v2, string KnownSeverityLevel (e.g. 'Information') on v3.
  • extractProperties(line, ignoreKeys): extract properties from log line
    • line: log line record object
    • ignoreKeys: configured ignore keys
  • properties:
    • ignoreKeys: configured ignore keys, defaults to ['hostname', 'pid', 'level', 'time', 'msg']

Telemetrish object

  • severity: pino log level mapped to the loaded SDK's severity enum (numeric on v2, string on v3 — see convertLevel)
  • msg: log message string
  • properties: telemetry properties object, filtered through ignore keys
  • tagOverrides?: object passed through from the pino log record (only honoured by v2's trackTrace/trackException; ignored by v3)
  • tracing?: distributed-trace correlation ids ({ traceId, spanId, traceFlags?, traceState? }); the default track function forwards these to both v2 and v3 — see Distributed tracing
  • exception?: logged Error if any
  • [k: string]: any other properties that facilitate telemetry logging

class FakeApplicationInsights(setupString)

Intercept calls to application insights. Works against both v2 and v3 wire formats — the v2 SDK posts gzipped NDJSON, v3 posts an application/json array, and the helper decodes both.

  • constructor(setupString)
    • setupString: connection string used to derive the ingestion endpoint. The constructor does not install any nock interceptors — they're set up lazily on the first expect…() call so an idle instance leaves no global state behind.
  • expectMessageData(): Expect tracked message, returns Promise<FakeCollectData>
  • expectEventData(): Expect tracked event, returns Promise<FakeCollectData>
  • expectExceptionData(): Expect tracked exception, returns Promise<FakeCollectData>
  • expectTelemetryType(telemetryType: string): Expect tracked telemetry type, returns Promise<FakeCollectData>
    • telemetryType: Telemetry type string (e.g. 'MessageData', 'EventData', 'ExceptionData', 'MetricData')
  • expect(count = 1): Expect tracked telemetrys, returns promise with list of FakeCollectData. Resolves once at least count items have been accumulated across one or more requests; the resolved array may be longer than count when a single request batch tips the total over.
    • count: wait for at least this many tracked telemetrys before returning, default is 1
  • reset(): Reset expected faked Application Insights calls. Removes only the interceptors this instance registered — never calls nock.cleanAll(), so any nock state your test suite has set up is left untouched. The dispatcher is re-installed lazily on the next expect…() call.
  • properties:
    • client: a TelemetryClient instance (the SDK version that's installed)

Caveats

  • The dispatcher resolves multiple pending expectations from a single request body, which is required because v3 batches multiple telemetry items (e.g. a MessageData and an ExceptionData produced by one logger.error(err, msg)) into one HTTP POST.
  • A persistent fallback interceptor replies 200 { itemsReceived, itemsAccepted, errors: [] } for any trailing telemetry sent after all expectations are satisfied — without it the SDK logs Ingestion endpoint could not be reached because nock raises No match for request.
  • Tests that need v3 to flush eagerly (the OTel BatchLogRecordProcessor defaults to ~5s scheduledDelayMillis and v3 ignores maxBatchSize) should patch TelemetryClient.prototype.{trackTrace,trackException,…} to call client.flush() after each call — see test/src/log-transport-test.js for the pattern.

Example

import { randomUUID } from 'node:crypto';
import { pino } from 'pino';

import compose from '@0dep/pino-applicationinsights';
import { FakeApplicationInsights } from '@0dep/pino-applicationinsights/fake-applicationinsights';

describe('test logger', () => {
  const connectionString = `InstrumentationKey=${randomUUID()};IngestionEndpoint=https://ingestion.local;LiveEndpoint=https://livemonitor.local/`;

  let fakeAI;
  before(() => {
    fakeAI = new FakeApplicationInsights(connectionString);
  });
  after(() => {
    fakeAI.reset();
  });

  it('log event track event', async () => {
    const transport = compose({
      track(chunk) {
        const { time, properties } = chunk;
        this.trackEvent({ name: 'my event', time, properties, measurements: { logins: 1 } });
      },
      connectionString,
      config: { maxBatchSize: 1, disableStatsbeat: true },
    });
    const logger = pino(transport);

    const expectMessage = fakeAI.expectEventData();

    logger.info({ bar: 'baz' }, 'foo');

    const msg = await expectMessage;

    expect(msg.body.data.baseData).to.deep.include({
      properties: { bar: 'baz' },
      measurements: { logins: 1 },
      name: 'my event',
    });

    transport.destroy();
  });
});

FakeCollectData

An object representing the request sent to application insights.

  • uri: request uri
  • method: request method
  • headers: request headers object
  • body:
    • ver: some version number, usually 1
    • sampleRate: sample rate number, usually 100
    • tags: object with tags. v2 only populates tagOverrides here; v3's wire tags come from OTel resource attributes. Tag names are available on TelemetryClient.context.keys for both versions, e.g.:
      • ai.application.ver: your package.json version
      • ai.cloud.roleInstance: hostname
      • ai.device.osVersion / osPlatform / osArchitecture
      • ai.cloud.role
      • ai.internal.sdkVersion: applicationinsights package version, e.g. node:2.9.8 for v2, node:3.x.y for v3
      • [tag name]: any other tag found under TelemetryClient.context.keys
    • data:
      • baseType: telemetry type string
      • baseData:
        • ver: some version number, usually 2 for some reason
        • properties: telemetry properties object
        • [message]: logged message when tracking trace
        • [severityLevel]: applicationinsights severity level when tracking trace and exception. Numeric (04) on v2; string ('Verbose''Critical') on v3.
        • [exceptions]: list of exceptions when tracking exception
          • message: error message
          • typeName: error class name
          • hasFullStack: v2 only; v3's exception envelope omits this flag
          • parsedStack: stack frames parsed as objects. Frame shape differs between v2 (fileName, level, method, line, assembly) and v3 (additional fields, no v2 _baseSize/sizeInBytes semantics)
        • [x: string]: any other telemetry property
    • iKey: applicationinsights instrumentation key
    • name: some ms name with iKey and the tracked type
    • time: log time

Application Insights v2 vs v3

This library targets applicationinsights >= 2 < 4. The v3 SDK is a thin "classic-API" shim over the OpenTelemetry-based @azure/monitor-opentelemetry-exporter; it intentionally preserves the TelemetryClient constructor and trackTrace / trackException / trackEvent / trackMetric methods but drops a number of v2 surface details. The library masks most of these for you, but a few are visible to consumers.

Severity values

  • v2's Contracts.SeverityLevel is a numeric enum: Verbose=0, Information=1, Warning=2, Error=3, Critical=4.
  • v3 removed it and exposes KnownSeverityLevel with string values: 'Verbose', 'Information', 'Warning', 'Error', 'Critical'.
  • compose() picks the right one at module load (Contracts.SeverityLevel ?? KnownSeverityLevel ?? numeric-fallback). Custom track functions just pass chunk.severity through to the SDK.
  • The wire severityLevel field on the AI envelope is therefore numeric under v2 and string under v3. Don't hardcode a numeric 0..4 comparison if you need to support both.

tagOverrides

  • v2 honours tagOverrides on trackTrace / trackException / etc. and copies them into the request envelope's tags map.
  • The v3 shim ignores tagOverrides entirely. The wire tags map is built from OTel resource attributes (e.g. service.name, service.instance.id) and the TelemetryClient constructor's useGlobalProviders settings. To set role/instance/user info on v3, use the OTel resource API rather than tagOverrides.
  • For distributed-trace correlation (ai.operation.id / ai.operation.parentId) that works on both versions, use the tracing field in the pino mixin — see Distributed tracing.

client.config.*

  • v2's client.config accepts dozens of knobs (maxBatchSize, endpointUrl, samplingPercentage, disableAppInsights, enableAutoCollect*, …) and applies them at runtime.
  • v3 exposes a client.config object but most v2 knobs are no-ops or warn (The maxBatchSize configuration option is not supported by the shim). v3 batches via OTel's BatchLogRecordProcessor (default ~5s scheduledDelayMillis); call await client.flush() if you need eager export.
  • The library's applyClientConfig merges your config into client.config regardless of version, but you should expect v3 to silently drop most of it.

Disabling statsbeat

  • On v2, config.disableStatsbeat: true calls client.getStatsbeat().enable(false).
  • On v3, client.getStatsbeat() returns null and config.disableStatsbeat: true is a no-op. The library does not mutate the APPLICATION_INSIGHTS_NO_STATSBEAT env var on your behalf — set it yourself before any applicationinsights import. Two recipes:
    1. Main process — set the env var before importing the SDK:

      process.env.APPLICATION_INSIGHTS_NO_STATSBEAT = 'disable';
      const compose = (await import('@0dep/pino-applicationinsights')).default;
    2. pino.transport worker — pass it via the target's worker.env so the worker thread inherits the disabled flag without polluting the main process env. See example/logger.js:

      pino.transport({
        targets: [
          {
            target: '@0dep/pino-applicationinsights',
            worker: { env: { ...process.env, APPLICATION_INSIGHTS_NO_STATSBEAT: 'disable' } },
            options: { connectionString, config: { maxBatchSize: 1 } },
          },
        ],
      });

Connection string vs bare instrumentation key

  • v2 accepts either a full InstrumentationKey=…;IngestionEndpoint=…;… connection string or a bare instrumentation key (UUID).
  • v3's TelemetryClient constructor only accepts the full connection string and throws if given a bare key. Always pass the full string for cross-version code.

Endpoint URL

  • v2 exposes the full endpointUrl (including /v2.1/track) on client.config.endpointUrl.
  • v3 dropped that field. The library and FakeApplicationInsights parse the connection string themselves (see src/connection-string.js) — there is no public lookup on the v3 client.

Contracts.ContextTagKeys

  • v2 ships Contracts.ContextTagKeys as a constructor (new Contracts.ContextTagKeys()).
  • v3 dropped that export. The same key strings are still available on every TelemetryClient instance via client.context.keys. Prefer client.context.keys in new code — it works on both versions.

Exception envelopes

The v2 and v3 SDKs format ExceptionData envelopes differently:

| Field | v2 | v3 | | ---------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | | exceptions[i].hasFullStack | true for fully-parsed stacks | not emitted | | exceptions[i].typeName | error class name | error class name | | exceptions[i].parsedStack | array of { fileName, line, method, assembly, level } | array with similar fields but a different shape and ordering — don't depend on field-by-field equality |

If you assert against captured exception envelopes in tests, gate v2-specific fields behind a version check (or use mock.method against TelemetryClient.prototype.trackException to assert at the SDK API surface instead of the wire — see test/src/module-mock-test.js for the pattern).