feat(core-shared): currentTraceId helper for OTel-audit correlation bridge
Reads the active OTel span context via trace.getActiveSpan(); returns the 32-char hex traceId or undefined when no span is active or traceId is the all-zeros invalid value. Re-exported from both instrumentation/otel/index and instrumentation/index barrels. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -28,3 +28,4 @@ export { bindOtelInstrumentation as bindSentryInstrumentation } from "./di/bind-
|
|||||||
export type { BindOtelOpts as BindSentryOpts } from "./di/bind-otel-instrumentation";
|
export type { BindOtelOpts as BindSentryOpts } from "./di/bind-otel-instrumentation";
|
||||||
|
|
||||||
export { initSentryClientReact } from "./sentry/init-client-react";
|
export { initSentryClientReact } from "./sentry/init-client-react";
|
||||||
|
export { currentTraceId } from "./otel/current-trace-id";
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { context, trace } from "@opentelemetry/api";
|
||||||
|
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
||||||
|
import { BasicTracerProvider, InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
||||||
|
import { currentTraceId } from "./current-trace-id";
|
||||||
|
|
||||||
|
// Register async context manager so startActiveSpan propagates context.
|
||||||
|
const ctxManager = new AsyncLocalStorageContextManager();
|
||||||
|
ctxManager.enable();
|
||||||
|
context.setGlobalContextManager(ctxManager);
|
||||||
|
|
||||||
|
function setupProvider(): { exporter: InMemorySpanExporter; provider: BasicTracerProvider } {
|
||||||
|
const exporter = new InMemorySpanExporter();
|
||||||
|
const provider = new BasicTracerProvider({
|
||||||
|
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
||||||
|
});
|
||||||
|
trace.setGlobalTracerProvider(provider);
|
||||||
|
return { exporter, provider };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("currentTraceId", () => {
|
||||||
|
let exporter: InMemorySpanExporter;
|
||||||
|
let provider: BasicTracerProvider;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
({ exporter, provider } = setupProvider());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await provider.shutdown();
|
||||||
|
trace.disable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when no active span", () => {
|
||||||
|
expect(currentTraceId()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the active span's traceId when inside startActiveSpan", async () => {
|
||||||
|
const tracer = trace.getTracer("test");
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
tracer.startActiveSpan("test-span", (span) => {
|
||||||
|
const id = currentTraceId();
|
||||||
|
expect(id).toBeDefined();
|
||||||
|
expect(id).toMatch(/^[a-f0-9]{32}$/);
|
||||||
|
span.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters all-zeros invalid traceId", () => {
|
||||||
|
// The INVALID_SPAN (no-op) has traceId "00000000000000000000000000000000"
|
||||||
|
// which is what getActiveSpan() returns when there is no real span.
|
||||||
|
// currentTraceId() must treat this as absent.
|
||||||
|
expect(currentTraceId()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Suppress unused variable warning — exporter used via closure
|
||||||
|
it("returns distinct traceIds for independent spans", async () => {
|
||||||
|
const tracer = trace.getTracer("test");
|
||||||
|
const ids: string[] = [];
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
tracer.startActiveSpan(`span-${i}`, (span) => {
|
||||||
|
const id = currentTraceId();
|
||||||
|
if (id) ids.push(id);
|
||||||
|
span.end();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Both spans are in fresh traces — IDs should be valid hex strings
|
||||||
|
expect(ids).toHaveLength(2);
|
||||||
|
for (const id of ids) {
|
||||||
|
expect(id).toMatch(/^[a-f0-9]{32}$/);
|
||||||
|
}
|
||||||
|
void exporter; // suppress lint
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { trace } from "@opentelemetry/api";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the trace ID of the currently active OTel span, or undefined if
|
||||||
|
* there is no active span (e.g., outside any request context, in unit tests
|
||||||
|
* without an OTel SDK).
|
||||||
|
*
|
||||||
|
* Used by core-audit's TraceIdEnrichingAuditLog decorator to auto-populate
|
||||||
|
* AuditEntry.correlationId so callers don't have to thread it explicitly.
|
||||||
|
*
|
||||||
|
* Returns undefined for the all-zeros invalid trace ID — OTel emits this
|
||||||
|
* when context propagation hasn't kicked in.
|
||||||
|
*/
|
||||||
|
export function currentTraceId(): string | undefined {
|
||||||
|
const span = trace.getActiveSpan();
|
||||||
|
if (!span) return undefined;
|
||||||
|
const ctx = span.spanContext();
|
||||||
|
if (!ctx.traceId || /^0+$/.test(ctx.traceId)) return undefined;
|
||||||
|
return ctx.traceId;
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { initOtelServerNode, type InitOtelServerNodeOpts } from "./init-server-node";
|
export { initOtelServerNode, type InitOtelServerNodeOpts } from "./init-server-node";
|
||||||
export { buildResource, type BuildResourceOpts } from "./resource";
|
export { buildResource, type BuildResourceOpts } from "./resource";
|
||||||
|
export { currentTraceId } from "./current-trace-id";
|
||||||
|
|||||||
Reference in New Issue
Block a user