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>
77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
// 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),
|
|
]),
|
|
);
|
|
}
|