feat(core-shared): OtelLogger impl using @opentelemetry/api-logs
Implements ILogger via OTel Logs API. captureException emits an ERROR log record with exception.* semantic conventions and double-report guard; captureMessage maps ILogger levels to OTel SeverityNumber; addBreadcrumb attaches span events; setUser sets user.id span attribute. 15 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
76
packages/core-shared/src/instrumentation/otel/otel-logger.ts
Normal file
76
packages/core-shared/src/instrumentation/otel/otel-logger.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
// packages/core-shared/src/instrumentation/otel/otel-logger.ts
|
||||
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
|
||||
import { trace } from "@opentelemetry/api";
|
||||
import { isReported, markReported } from "../reported-flag";
|
||||
import type { ILogger, Breadcrumb, CaptureContext } from "../logger.interface";
|
||||
|
||||
export class OtelLogger implements ILogger {
|
||||
private readonly logger = logs.getLogger("@repo/core-shared", "1.0.0");
|
||||
|
||||
captureException(err: unknown, ctx?: CaptureContext): void {
|
||||
if (isReported(err)) return;
|
||||
markReported(err);
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
this.logger.emit({
|
||||
severityNumber: SeverityNumber.ERROR,
|
||||
severityText: "ERROR",
|
||||
body: error.message,
|
||||
attributes: {
|
||||
"exception.type": error.name,
|
||||
"exception.message": error.message,
|
||||
"exception.stacktrace": error.stack ?? "",
|
||||
...flattenTags(ctx?.tags),
|
||||
...flattenExtras(ctx?.extras),
|
||||
...(ctx?.fingerprint ? { "sentry.fingerprint": ctx.fingerprint.join("|") } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
captureMessage(msg: string, level?: "info" | "warning" | "error", ctx?: CaptureContext): void {
|
||||
const severityNumber =
|
||||
level === "error"
|
||||
? SeverityNumber.ERROR
|
||||
: level === "warning"
|
||||
? SeverityNumber.WARN
|
||||
: SeverityNumber.INFO;
|
||||
const severityText =
|
||||
level === "error" ? "ERROR" : level === "warning" ? "WARNING" : "INFO";
|
||||
this.logger.emit({
|
||||
severityNumber,
|
||||
severityText,
|
||||
body: msg,
|
||||
attributes: { ...flattenTags(ctx?.tags), ...flattenExtras(ctx?.extras) },
|
||||
});
|
||||
}
|
||||
|
||||
addBreadcrumb(b: Breadcrumb): void {
|
||||
const span = trace.getActiveSpan();
|
||||
if (!span) return;
|
||||
span.addEvent(b.message, {
|
||||
"breadcrumb.category": b.category,
|
||||
"breadcrumb.level": b.level ?? "info",
|
||||
...(b.data ? flattenExtras(b.data) : {}),
|
||||
});
|
||||
}
|
||||
|
||||
setUser(user: { id: string } | null): void {
|
||||
const span = trace.getActiveSpan();
|
||||
if (!span) return;
|
||||
span.setAttribute("user.id", user?.id ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
function flattenTags(tags?: Record<string, string>): Record<string, string> {
|
||||
if (!tags) return {};
|
||||
return Object.fromEntries(Object.entries(tags).map(([k, v]) => [`tag.${k}`, v]));
|
||||
}
|
||||
|
||||
function flattenExtras(extras?: Record<string, unknown>): Record<string, string> {
|
||||
if (!extras) return {};
|
||||
return Object.fromEntries(
|
||||
Object.entries(extras).map(([k, v]) => [
|
||||
`extra.${k}`,
|
||||
typeof v === "string" ? v : String(v),
|
||||
]),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user