Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/instrumentation-pino/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<!-- markdownlint-disable MD007 MD034 -->
# Changelog

## [Unreleased]

### Features

* **instrumentation-pino:** use the Logs API `exception` field for pino error records during log sending, including support for custom pino `errorKey` ([#3425](https://github.com/open-telemetry/opentelemetry-js-contrib/issues/3425))

## [0.62.0](https://github.com/open-telemetry/opentelemetry-js-contrib/compare/instrumentation-pino-v0.61.0...instrumentation-pino-v0.62.0) (2026-04-29)


Expand Down
1 change: 1 addition & 0 deletions packages/instrumentation-pino/src/instrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class PinoInstrumentation extends InstrumentationBase<PinoInstrumentation
);
const otelStream = new OTelPinoStream({
messageKey: logger[moduleExports.symbols.messageKeySym],
errorKey: logger[moduleExports.symbols.errorKeySym],
levels: logger.levels,
otelTimestampFromTime,
});
Expand Down
21 changes: 21 additions & 0 deletions packages/instrumentation-pino/src/log-sending-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export function getTimeConverter(pinoLogger: any, pinoMod: any) {

interface OTelPinoStreamOptions {
messageKey: string;
errorKey: string;
levels: any; // Pino.LevelMapping
otelTimestampFromTime: (time: any) => number;
}
Expand All @@ -122,6 +123,7 @@ interface OTelPinoStreamOptions {
export class OTelPinoStream extends Writable {
declare private _otelLogger: Logger;
declare private _messageKey: string;
declare private _errorKey: string;
declare private _levels;
declare private _otelTimestampFromTime;

Expand All @@ -133,6 +135,7 @@ export class OTelPinoStream extends Writable {
// for auto-configuration in newer pino versions. The event currently does
// not include the `timeSym` value that is needed here, however.
this._messageKey = options.messageKey;
this._errorKey = options.errorKey;
this._levels = options.levels;
this._otelTimestampFromTime = options.otelTimestampFromTime;

Expand Down Expand Up @@ -176,6 +179,7 @@ export class OTelPinoStream extends Writable {
const {
time,
[this._messageKey]: body,
[this._errorKey]: exception,
level, // eslint-disable-line @typescript-eslint/no-unused-vars

// The typical Pino `hostname` and `pid` fields are removed because they
Expand Down Expand Up @@ -223,9 +227,26 @@ export class OTelPinoStream extends Writable {
severityText: this._levels.labels[lastLevel],
body,
attributes,
exception: normalizeException(exception),
};

this._otelLogger.emit(otelRec);
callback();
}
}

function normalizeException(exception: unknown): unknown {
if (!exception || typeof exception !== 'object' || Array.isArray(exception)) {
return exception;
}

const exceptionObject = exception as Record<string, unknown>;
if (
typeof exceptionObject['type'] === 'string' &&
typeof exceptionObject['name'] !== 'string'
) {
exceptionObject['name'] = exceptionObject['type'];
}

return exceptionObject;
}
38 changes: 37 additions & 1 deletion packages/instrumentation-pino/test/pino.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import * as sinon from 'sinon';
import { INVALID_SPAN_CONTEXT, context, trace, Span } from '@opentelemetry/api';
import { diag, DiagLogLevel } from '@opentelemetry/api';
import { hrTimeToMilliseconds } from '@opentelemetry/core';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
import {
ATTR_EXCEPTION_MESSAGE,
ATTR_EXCEPTION_STACKTRACE,
ATTR_EXCEPTION_TYPE,
ATTR_SERVICE_NAME,
} from '@opentelemetry/semantic-conventions';
import { resourceFromAttributes } from '@opentelemetry/resources';
import {
InMemorySpanExporter,
Expand Down Expand Up @@ -536,6 +541,37 @@ describe('PinoInstrumentation', () => {
assert.strictEqual(logRecords[2].body, 'second msg at trace');
});

it('emits exceptions using the Logs API "exception" field', () => {
const err = new TypeError('boom');
logger.error(err, 'error happened');

const logRecords = memExporter.getFinishedLogRecords();
const rec = logRecords[logRecords.length - 1];
assert.strictEqual(rec.body, 'error happened');
assert.strictEqual(rec.attributes[ATTR_EXCEPTION_TYPE], 'TypeError');
assert.strictEqual(rec.attributes[ATTR_EXCEPTION_MESSAGE], 'boom');
assert.ok(rec.attributes[ATTR_EXCEPTION_STACKTRACE]);

// The original pino error field should not be emitted as a regular attribute.
assert.strictEqual(rec.attributes.err, undefined);
});

it('uses custom errorKey with the Logs API "exception" field', () => {
logger = pino({ errorKey: 'myErr' }, stream);
logger.error(new Error('custom key error'), 'error with custom key');

const logRecords = memExporter.getFinishedLogRecords();
const rec = logRecords[logRecords.length - 1];
assert.strictEqual(rec.body, 'error with custom key');
assert.strictEqual(rec.attributes[ATTR_EXCEPTION_TYPE], 'Error');
assert.strictEqual(
rec.attributes[ATTR_EXCEPTION_MESSAGE],
'custom key error'
);
assert.ok(rec.attributes[ATTR_EXCEPTION_STACKTRACE]);
assert.strictEqual(rec.attributes.myErr, undefined);
});

it('emits log records from child logger at lower level', () => {
const logRecords = memExporter.getFinishedLogRecords();

Expand Down
Loading