diff --git a/.gitignore b/.gitignore index f7f8ce25..505b1ec7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ tmp-* chicken.* .vscode .idea -e2e.* \ No newline at end of file +e2e.* +.claude/.planning/ diff --git a/jest.json b/jest.json index 6495a874..0a7d2614 100644 --- a/jest.json +++ b/jest.json @@ -6,7 +6,7 @@ "**/*.spec.ts" ], "transform": { - "^.+\\.ts$": "ts-jest" + "^.+\\.ts$": ["ts-jest", { "tsconfig": "tsconfig-base.json", "isolatedModules": true }] }, "transformIgnorePatterns": [ "/node_modules/(?!(axios)/)" diff --git a/packages/logging/src/otelProvider/internal-types.ts b/packages/logging/src/otelProvider/internal-types.ts index 13bdeda9..489d8507 100644 --- a/packages/logging/src/otelProvider/internal-types.ts +++ b/packages/logging/src/otelProvider/internal-types.ts @@ -56,6 +56,7 @@ export interface EventAttributesException { type?: string; message?: string; stacktrace?: string; + cause?: EventAttributesException | string; } export enum OtelSeverity { diff --git a/packages/logging/src/otelProvider/otelProvider.spec.ts b/packages/logging/src/otelProvider/otelProvider.spec.ts index 082a4e74..f0481784 100644 --- a/packages/logging/src/otelProvider/otelProvider.spec.ts +++ b/packages/logging/src/otelProvider/otelProvider.spec.ts @@ -1,6 +1,6 @@ import { OtelProviderError, otelProviderFactory } from "./otelProvider"; import { DvelopLogEvent, DbRequest, HttpResponse, IncomingHttpRequest, OutgoingHttpRequest, DvelopLogLevel } from "../logger/log-event"; -import { OtelEvent, OtelSeverity } from "./internal-types"; +import { EventAttributesException, OtelEvent, OtelSeverity } from "./internal-types"; import { DvelopContext, TraceContext } from "@dvelop-sdk/core"; describe("otel provider", () => { @@ -587,10 +587,11 @@ describe("otel provider", () => { appName: "test", transports: [async otelMessage => { const json: OtelEvent = JSON.parse(otelMessage); expect(json.attr).toBeDefined(); - expect(json.attr.exception).toBeDefined(); - expect(json.attr.exception.type).toEqual("Error"); - expect(json.attr.exception.message).toEqual("some error"); - expect(json.attr.exception.stacktrace).toBeDefined(); + expect(json.attr?.exception).toBeDefined(); + expect(json.attr?.exception?.type).toEqual("Error"); + expect(json.attr?.exception?.message).toEqual("some error"); + expect(json.attr?.exception?.stacktrace).toBeDefined(); + expect(json.attr?.exception?.cause).toBeUndefined(); done(); }] }); @@ -603,6 +604,70 @@ describe("otel provider", () => { }); }); + test("should set exception cause when cause is an Error", () => { + return new Promise(done => { + const provider = otelProviderFactory({ + appName: "test", transports: [async otelMessage => { + const json: OtelEvent = JSON.parse(otelMessage); + expect(json.attr?.exception?.cause).toBeDefined(); + expect((json.attr?.exception?.cause as EventAttributesException).type).toEqual("TypeError"); + expect((json.attr?.exception?.cause as EventAttributesException).message).toEqual("root cause"); + expect((json.attr?.exception?.cause as EventAttributesException).stacktrace).toBeDefined(); + done(); + }] + }); + + const cause = new TypeError("root cause"); + const event: DvelopLogEvent = { + error: Object.assign(new Error("wrapper error"), { cause }) + }; + + provider({}, event, "debug"); + }); + }); + + test("should set exception cause recursively for nested Error causes", () => { + return new Promise(done => { + const provider = otelProviderFactory({ + appName: "test", transports: [async otelMessage => { + const json: OtelEvent = JSON.parse(otelMessage); + const outerCause = json.attr?.exception?.cause as EventAttributesException; + expect(outerCause.message).toEqual("middle error"); + expect(outerCause.cause).toBeDefined(); + expect((outerCause.cause as EventAttributesException).message).toEqual("root cause"); + done(); + }] + }); + + const root = new Error("root cause"); + const middle = Object.assign(new Error("middle error"), { cause: root }); + const event: DvelopLogEvent = { + error: Object.assign(new Error("outer error"), { cause: middle }) + }; + + provider({}, event, "debug"); + }); + }); + + test("should JSON-stringify exception cause when cause is not an Error", () => { + return new Promise(done => { + const provider = otelProviderFactory({ + appName: "test", transports: [async otelMessage => { + const json: OtelEvent = JSON.parse(otelMessage); + expect(typeof json.attr?.exception?.cause).toEqual("string"); + expect(json.attr?.exception?.cause).toEqual(JSON.stringify({ code: 42 })); + done(); + }] + }); + + const event: DvelopLogEvent = { + error: Object.assign(new Error("some error"), { cause: { code: 42 } }) + }; + + provider({}, event, "debug"); + }); + }); + }); }); diff --git a/packages/logging/src/otelProvider/otelProvider.ts b/packages/logging/src/otelProvider/otelProvider.ts index 51ffdbb7..7fc2038a 100644 --- a/packages/logging/src/otelProvider/otelProvider.ts +++ b/packages/logging/src/otelProvider/otelProvider.ts @@ -9,7 +9,7 @@ import { LoggingError } from "../error"; * @category Error */ export class OtelProviderError extends LoggingError { - + constructor(message: string, originalError?: Error) { super(message, originalError); Object.setPrototypeOf(this, OtelProviderError.prototype); @@ -170,13 +170,18 @@ function mapDbAttribute(event: DvelopLogEvent): { db?: EventAttributesDb } { } function mapExceptionAttribute(event: DvelopLogEvent): { exception?: EventAttributesException } { + const mapErrorToAttributes = (error: Error): EventAttributesException => { + return { + message: error.message, + type: error.name, + stacktrace: error.stack, + cause: error.cause ? (error.cause instanceof Error ? mapErrorToAttributes(error.cause) : JSON.stringify(error.cause)) : undefined + }; + }; + if (event.error) { return { - exception: { - message: event.error.message, - type: event.error.name, - stacktrace: event.error.stack - } + exception: mapErrorToAttributes(event.error) }; } else { return {};