Observed since 20 Oct, 22:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Pino applicationinsights transport
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.
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:
tagOverridesis honoured byapplicationinsights@2only. The v3 classic-API shim ignores it — see Application Insights v2 vs v3. For cross-version distributed-trace correlation, passtracingfrom 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);
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:
{ [client.context.keys.operationId]: traceId, [client.context.keys.operationParentId]: spanId } into tagOverrides, which v2 copies to the wire envelope's tags map.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.
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.
track functionsCallers 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,
});
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.
compose(opts[, TelemetryTransformation]) => StreamBuild 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)
chunk: Telemetry:ish objectconfig: 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 optionsignoreKeys: optional pino ignore keys, used to filter telemetry properties, defaults to ['hostname', 'pid', 'level', 'time', 'msg']TelemetryTransformation: optional transformation stream extending TelemetryTransformationclass 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 setconfig: 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 objectconvertLevel(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 objectignoreKeys: configured ignore keysignoreKeys: configured ignore keys, defaults to ['hostname', 'pid', 'level', 'time', 'msg']severity: pino log level mapped to the loaded SDK's severity enum (numeric on v2, string on v3 — see convertLevel)msg: log message stringproperties: telemetry properties object, filtered through ignore keystagOverrides?: 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 tracingexception?: logged Error if any[k: string]: any other properties that facilitate telemetry loggingclass 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 1reset(): 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.client: a TelemetryClient instance (the SDK version that's installed)MessageData and an ExceptionData produced by one logger.error(err, msg)) into one HTTP POST.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.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.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();
});
});
FakeCollectDataAn object representing the request sent to application insights.
uri: request urimethod: request methodheaders: request headers objectbody:
ver: some version number, usually 1sampleRate: sample rate number, usually 100tags: 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 versionai.cloud.roleInstance: hostnameai.device.osVersion / osPlatform / osArchitectureai.cloud.roleai.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.keysdata:
baseType: telemetry type stringbaseData:
ver: some version number, usually 2 for some reasonproperties: telemetry properties object[message]: logged message when tracking trace[severityLevel]: applicationinsights severity level when tracking trace and exception. Numeric (0–4) on v2; string ('Verbose'–'Critical') on v3.[exceptions]: list of exceptions when tracking exception
message: error messagetypeName: error class namehasFullStack: v2 only; v3's exception envelope omits this flagparsedStack: 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 propertyiKey: applicationinsights instrumentation keyname: some ms name with iKey and the tracked typetime: log timeThis 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.
Contracts.SeverityLevel is a numeric enum: Verbose=0, Information=1, Warning=2, Error=3, Critical=4.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.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.tagOverridestagOverrides on trackTrace / trackException / etc. and copies them into the request envelope's tags map.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.ai.operation.id / ai.operation.parentId) that works on both versions, use the tracing field in the pino mixin — see Distributed tracing.client.config.*client.config accepts dozens of knobs (maxBatchSize, endpointUrl, samplingPercentage, disableAppInsights, enableAutoCollect*, …) and applies them at runtime.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.applyClientConfig merges your config into client.config regardless of version, but you should expect v3 to silently drop most of it.v3 buffers log records in OTel's BatchLogRecordProcessor and only exports them on the scheduledDelayMillis tick (~5s, not configurable through the shim) or at shutdown. For eager export call await client.flush() — but mind one OpenTelemetry gotcha:
@opentelemetry/sdk-logs >= 0.215 (pulled in by applicationinsights@3.15), a forceFlush() that overlaps an already in-flight flush returns immediately without exporting the records queued in that window — they fall back to the next ~5s tick. This is upstream behaviour (opentelemetry-js#6356); it is not surfaced in the applicationinsights changelog.await each flush before triggering the next rather than firing client.flush() concurrently. Awaiting it (the SDK's flush() resolves only after the export settles) keeps successive flushes from racing the guard.client.shutdown() to drain the batch processor.config.disableStatsbeat: true calls client.getStatsbeat().enable(false).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:
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;
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 } },
},
],
});
InstrumentationKey=…;IngestionEndpoint=…;… connection string or a bare instrumentation key (UUID).TelemetryClient constructor only accepts the full connection string and throws if given a bare key. Always pass the full string for cross-version code.endpointUrl (including /v2.1/track) on client.config.endpointUrl.FakeApplicationInsights parse the connection string themselves (see src/connection-string.js) — there is no public lookup on the v3 client.Contracts.ContextTagKeysContracts.ContextTagKeys as a constructor (new Contracts.ContextTagKeys()).TelemetryClient instance via client.context.keys. Prefer client.context.keys in new code — it works on both versions.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).
Download counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 2.1.0Registry latest | 16 Sept 2026 | 25 | |
| 2.0.3 | 24 Jul 2026 | 25 | |
| 2.0.2 | 14 Jun 2026 | 27 | |
| 2.0.1 | 27 Apr 2026 | 27 | |
| 2.0.0 | 27 Apr 2026 | 27 | |
| 1.1.3 | 14 Nov 2025 | 23 | |
| 1.1.2 | 17 Nov 2024 | 23 | |
| 1.1.1 | 16 Nov 2024 | 23 | |
| 1.1.0 | 16 Nov 2024 | 23 | |
| 1.0.1 | 27 Oct 2024 | 20 | |
| 1.0.0 | 21 May 2024 | 20 | |
| 0.1.2 | 16 Nov 2023 | 18 | |
| 0.1.1 | 13 Nov 2023 | 18 | |
| 0.1.0 | 13 Nov 2023 | 18 | |
| 0.0.7 | 10 Nov 2023 | 18 | |
| 0.0.6 | 10 Nov 2023 | 18 | |
| 0.0.5 | 9 Nov 2023 | 18 | |
| 0.0.4 | 9 Nov 2023 | 18 | |
| 0.0.3 | 8 Nov 2023 | 17 | |
| 0.0.2 | 8 Nov 2023 | 17 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 22:07 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:1c4c16d1902dbf39a34b52d918979d7e0c60ca1510383e67c59c4cfab0dcfcf3