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
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
* feat(sampler-composite): add ComposableAnnotatingSampler and ComposableRuleBasedSampler [#6305](https://github.com/open-telemetry/opentelemetry-js/pull/6305) @trentm
* feat(configuration): parse config for rc 3 [#6304](https://github.com/open-telemetry/opentelemetry-js/pull/6304) @maryliag
* feat(instrumentation): use the `internals: true` option with import-in-the-middle hook, allowing instrumentations to hook internal files in ES modules [#6344](https://github.com/open-telemetry/opentelemetry-js/pull/6344) @trentm
* feat(api-logs,sdk-logs): add log exception support and mapping [#6379](https://github.com/open-telemetry/opentelemetry-js/issues/6379) @iblancasa

### :bug: Bug Fixes

Expand Down
7 changes: 7 additions & 0 deletions experimental/packages/api-logs/src/types/LogRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ export interface LogRecord {
*/
attributes?: LogAttributes;

/**
* An exception (or error) associated with the log record.
*
* @experimental
*/
exception?: unknown;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[I'm not sure if this was discussed already.]

Whether to use "exception" or "error"? In JavaScript-land my impression is that "error" is the term for the thing we are talking about here. In Node.js, "exception" tends to lead one to Node's "uncaughtException" event. However, both terms are fine. MDN's https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error somewhat interchangeably uses both.

Given that (a) OTel specs use "exception", (b) adding this API is part of deprecating Span#recordException, and (c) there is some overloading / possible confusion with the "error" log/severity level, I'm fine with exception. I'd be fine with the property being called "error" as well.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I'd gravitate towards error, but the confusion with the severity level is a good point.
Let's keep exception. 👍


/**
* The Context associated with the LogRecord.
*/
Expand Down
3 changes: 2 additions & 1 deletion experimental/packages/sdk-logs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"dependencies": {
"@opentelemetry/api-logs": "0.212.0",
"@opentelemetry/core": "2.5.1",
"@opentelemetry/resources": "2.5.1"
"@opentelemetry/resources": "2.5.1",
"@opentelemetry/semantic-conventions": "^1.29.0"
}
}
57 changes: 57 additions & 0 deletions experimental/packages/sdk-logs/src/LogRecordImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ import type {
import * as api from '@opentelemetry/api';
import { timeInputToHrTime, InstrumentationScope } from '@opentelemetry/core';
import type { Resource } from '@opentelemetry/resources';
import {
ATTR_EXCEPTION_MESSAGE,
ATTR_EXCEPTION_STACKTRACE,
ATTR_EXCEPTION_TYPE,
} from '@opentelemetry/semantic-conventions';
import type { ReadableLogRecord } from './export/ReadableLogRecord';
import type { LogRecordLimits } from './types';
import { isLogAttributeValue } from './utils/validation';
Expand Down Expand Up @@ -102,6 +107,7 @@ export class LogRecordImpl implements ReadableLogRecord {
severityText,
body,
attributes = {},
exception,
context,
} = logRecord;

Expand All @@ -123,6 +129,9 @@ export class LogRecordImpl implements ReadableLogRecord {
this._logRecordLimits = _sharedState.logRecordLimits;
this._eventName = eventName;
this.setAttributes(attributes);
if (exception != null) {
this._setException(exception);
}
}

public setAttribute(key: string, value?: AnyValue) {
Expand Down Expand Up @@ -231,6 +240,54 @@ export class LogRecordImpl implements ReadableLogRecord {
return value;
}

private _setException(exception: unknown): void {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation here is transcribing what Span#recordException() currently does, which is fine.

for another PR/issue to discuss

I don't think this PR needs to change. I'm throwing out some ideas, because I've been around logging libs for too long.

There is a part of me that wants to do more here, e.g.:

  • handle a thrown number (not really a high priority, but for completeness :)
  • handle a exception.cause now that Error can take a cause option (since Node.js 16.9, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error)
  • handle other properties added to the Error instance. Specifically setting ATTR_EXCEPTION_TYPE to exception.code || exception.name feels slightly like a kludge. However, a better answer would likely require adding a new exception.* attribute to semconv, which might be an uphill battle.

err.cause

Inspired by Pino's serializers for Errors (https://github.com/pinojs/pino-std-serializers?tab=readme-ov-file#serializers) some options:

  1. Append the err.cause.stack, if any, to the ATTR_EXCEPTION_STACKTRACE (as Pino's err serializer does). I wonder if OTel Java is already doing this? Yes, I think it does, it is using Throwable.printStackTrace: https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk/common/src/main/java/io/opentelemetry/sdk/common/internal/DefaultExceptionAttributeResolver.java#L45-L58 https://docs.oracle.com/javase/8/docs/api/java/lang/Throwable.html#printStackTrace--
  2. Fight the power! and push for a deeply nested ATTR_EXCEPTION semconv property that can have nested cause values. :) I'm kidding. This isn't going to happen. Possibly a ATTR_EXCEPTION_CAUSE_* set of attributes could be added for one level of nesting, but I'm not sure that is valuable.

other exception properties

For example:

> assert.strictEqual(1, 2)
Uncaught AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

1 !== 2

    at REPL126:1:8
    at ContextifyScript.runInThisContext (node:vm:137:12)
    at REPLServer.defaultEval (node:repl:562:24)
    at bound (node:domain:433:15)
    at REPLServer.runBound [as eval] (node:domain:444:12)
    at REPLServer.onLine (node:repl:886:12)
    at REPLServer.emit (node:events:520:35)
    at REPLServer.emit (node:domain:489:12)
    at [_onLine] [as _onLine] (node:internal/readline/interface:465:12)
    at [_line] [as _line] (node:internal/readline/interface:953:18) {
  generatedMessage: true,
  code: 'ERR_ASSERTION',
  actual: 1,
  expected: 2,
  operator: 'strictEqual',
  diff: 'simple'
}

Copy link
Copy Markdown
Contributor Author

@iblancasa iblancasa Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • handle a thrown number (not really a high priority, but for completeness :)

Adding.

Interesting. Maybe we can propose this to the semantic conventions since it makes sense to me. What do you think?

  • handle other properties added to the Error instance. Specifically setting ATTR_EXCEPTION_TYPE to exception.code || exception.name feels slightly like a kludge. However, a better answer would likely require adding a new exception.* attribute to semconv, which might be an uphill battle.

Maybe. I can draft a PR on top of this one and create a proposal from semcon.

Your suggestions sound very reasonable to me and I guess those also make sense for other languages too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be worth looking at the discussions that led to the exception.* attributes in the spec/semconv to see if cause and/or code (or other exception attributes) were discussed.

I think it would be worthwhile to have a follow-up issue to consider adding exception.cause?.stack to the ATTR_EXCEPTION_STACKTRACE attribute.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Between today and tomorrow I will do some research and create one proposal if it doesn't exist yet. I will mention you and crosslink this PR to provide some context.

let hasMinimumAttributes = false;

if (typeof exception === 'string' || typeof exception === 'number') {
if (!Object.hasOwn(this.attributes, ATTR_EXCEPTION_MESSAGE)) {
this.setAttribute(ATTR_EXCEPTION_MESSAGE, String(exception));
}
hasMinimumAttributes = true;
} else if (exception && typeof exception === 'object') {
const exceptionObj = exception as {
code?: string | number;
name?: string;
message?: string;
stack?: string;
};

if (exceptionObj.code) {
if (!Object.hasOwn(this.attributes, ATTR_EXCEPTION_TYPE)) {
this.setAttribute(ATTR_EXCEPTION_TYPE, exceptionObj.code.toString());
}
hasMinimumAttributes = true;
} else if (exceptionObj.name) {
if (!Object.hasOwn(this.attributes, ATTR_EXCEPTION_TYPE)) {
this.setAttribute(ATTR_EXCEPTION_TYPE, exceptionObj.name);
}
hasMinimumAttributes = true;
}

if (exceptionObj.message) {
if (!Object.hasOwn(this.attributes, ATTR_EXCEPTION_MESSAGE)) {
this.setAttribute(ATTR_EXCEPTION_MESSAGE, exceptionObj.message);
}
hasMinimumAttributes = true;
}

if (exceptionObj.stack) {
if (!Object.hasOwn(this.attributes, ATTR_EXCEPTION_STACKTRACE)) {
this.setAttribute(ATTR_EXCEPTION_STACKTRACE, exceptionObj.stack);
Comment thread
iblancasa marked this conversation as resolved.
}
hasMinimumAttributes = true;
}
}

if (!hasMinimumAttributes) {
api.diag.warn(`Failed to record an exception ${exception}`);
}
}

private _truncateToLimitUtil(value: string, limit: number): string {
if (value.length <= limit) {
return value;
Expand Down
85 changes: 85 additions & 0 deletions experimental/packages/sdk-logs/test/common/LogRecord.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import { AnyValue } from '@opentelemetry/api-logs';
import type { HrTime } from '@opentelemetry/api';
import { hrTimeToMilliseconds, timeInputToHrTime } from '@opentelemetry/core';
import { defaultResource } from '@opentelemetry/resources';
import {
ATTR_EXCEPTION_MESSAGE,
ATTR_EXCEPTION_STACKTRACE,
ATTR_EXCEPTION_TYPE,
} from '@opentelemetry/semantic-conventions';

import {
LogRecordLimits,
Expand Down Expand Up @@ -156,6 +161,86 @@ describe('LogRecord', () => {
attr2: 123,
});
});

it('should set exception attributes from exception', () => {
const error = new Error('boom');
const logRecordData: logsAPI.LogRecord = {
exception: error,
};
const { logRecord } = setup(undefined, logRecordData);

assert.strictEqual(
logRecord.attributes[ATTR_EXCEPTION_MESSAGE],
error.message
);
assert.strictEqual(logRecord.attributes[ATTR_EXCEPTION_TYPE], error.name);
if (error.stack) {
assert.strictEqual(
logRecord.attributes[ATTR_EXCEPTION_STACKTRACE],
error.stack
);
}
});

it('should not overwrite user-provided exception attributes', () => {
const error = new Error('boom');
const logRecordData: logsAPI.LogRecord = {
exception: error,
attributes: {
[ATTR_EXCEPTION_MESSAGE]: 'user message',
[ATTR_EXCEPTION_TYPE]: 'CustomError',
},
};
const { logRecord } = setup(undefined, logRecordData);

assert.strictEqual(
logRecord.attributes[ATTR_EXCEPTION_MESSAGE],
'user message'
);
assert.strictEqual(
logRecord.attributes[ATTR_EXCEPTION_TYPE],
'CustomError'
);
});

it('should set exception.message for string exceptions', () => {
const logRecordData: logsAPI.LogRecord = {
exception: 'boom',
};
const { logRecord } = setup(undefined, logRecordData);

assert.strictEqual(logRecord.attributes[ATTR_EXCEPTION_MESSAGE], 'boom');
});

it('should set exception.message for numeric exceptions', () => {
const logRecordData: logsAPI.LogRecord = {
exception: 42,
};
const { logRecord } = setup(undefined, logRecordData);

assert.strictEqual(logRecord.attributes[ATTR_EXCEPTION_MESSAGE], '42');
});

it('should warn when exception has no useful fields', () => {
const warnSpy = sinon.stub(diag, 'warn');
const logRecordData: logsAPI.LogRecord = {
exception: {} as unknown,
};

setup(undefined, logRecordData);

assert.ok(warnSpy.calledOnce);
warnSpy.restore();
});

it('should set exception.type from code', () => {
const logRecordData: logsAPI.LogRecord = {
exception: { code: 12 },
};
const { logRecord } = setup(undefined, logRecordData);

assert.strictEqual(logRecord.attributes[ATTR_EXCEPTION_TYPE], '12');
});
});

describe('setAttribute', () => {
Expand Down
3 changes: 3 additions & 0 deletions experimental/packages/sdk-logs/tsconfig.esm.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
{
"path": "../../../packages/opentelemetry-resources"
},
{
"path": "../../../semantic-conventions"
},
{
"path": "../api-logs"
}
Expand Down
3 changes: 3 additions & 0 deletions experimental/packages/sdk-logs/tsconfig.esnext.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
{
"path": "../../../packages/opentelemetry-resources"
},
{
"path": "../../../semantic-conventions"
},
{
"path": "../api-logs"
}
Expand Down
3 changes: 3 additions & 0 deletions experimental/packages/sdk-logs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
{
"path": "../../../packages/opentelemetry-resources"
},
{
"path": "../../../semantic-conventions"
},
{
"path": "../api-logs"
}
Expand Down
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.