Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Complete the telemetry module with all acceptance criteria: - Add service.version resource attribute from package.json - Add deployment.environment resource attribute from env vars - Add trace sampling configuration with OTEL_TRACES_SAMPLER_ARG - Implement ParentBasedSampler for consistent distributed tracing - Add comprehensive tests for SpanContextService (15 tests) - Add comprehensive tests for LlmTelemetryDecorator (29 tests) - Fix type safety issues (JSON.parse typing, template literals) - Add security linter exception for package.json read Test coverage: 74 tests passing, 85%+ coverage on telemetry module. Fixes #312 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
225 lines
6.5 KiB
TypeScript
225 lines
6.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { SpanContextService } from "./span-context.service";
|
|
import { context, trace, type Span, type Context } from "@opentelemetry/api";
|
|
import { AsyncHooksContextManager } from "@opentelemetry/context-async-hooks";
|
|
|
|
describe("SpanContextService", () => {
|
|
let service: SpanContextService;
|
|
let contextManager: AsyncHooksContextManager;
|
|
|
|
beforeEach(() => {
|
|
// Set up context manager for proper context propagation in tests
|
|
contextManager = new AsyncHooksContextManager();
|
|
contextManager.enable();
|
|
context.setGlobalContextManager(contextManager);
|
|
service = new SpanContextService();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up context manager
|
|
contextManager.disable();
|
|
context.disable();
|
|
});
|
|
|
|
describe("getActiveSpan", () => {
|
|
it("should return undefined when no span is active", () => {
|
|
const activeSpan = service.getActiveSpan();
|
|
expect(activeSpan).toBeUndefined();
|
|
});
|
|
|
|
it("should return the active span when one exists", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("test-span");
|
|
|
|
const result = context.with(trace.setSpan(context.active(), span), () => {
|
|
return service.getActiveSpan();
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result).toBe(span);
|
|
span.end();
|
|
});
|
|
});
|
|
|
|
describe("getContext", () => {
|
|
it("should return the current context", () => {
|
|
const ctx = service.getContext();
|
|
expect(ctx).toBeDefined();
|
|
});
|
|
|
|
it("should return the active context", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("test-span");
|
|
|
|
const result = context.with(trace.setSpan(context.active(), span), () => {
|
|
return service.getContext();
|
|
});
|
|
|
|
expect(result).toBeDefined();
|
|
span.end();
|
|
});
|
|
});
|
|
|
|
describe("with", () => {
|
|
it("should execute function within the provided context", () => {
|
|
const customContext = context.active();
|
|
let executedInContext = false;
|
|
|
|
const result = service.with(customContext, () => {
|
|
executedInContext = true;
|
|
return "test-result";
|
|
});
|
|
|
|
expect(executedInContext).toBe(true);
|
|
expect(result).toBe("test-result");
|
|
});
|
|
|
|
it("should propagate the context to the function", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("test-span");
|
|
const spanContext = trace.setSpan(context.active(), span);
|
|
|
|
const result = service.with(spanContext, () => {
|
|
const activeSpan = trace.getActiveSpan();
|
|
return activeSpan;
|
|
});
|
|
|
|
expect(result).toBe(span);
|
|
span.end();
|
|
});
|
|
|
|
it("should handle exceptions in the function", () => {
|
|
const customContext = context.active();
|
|
|
|
expect(() => {
|
|
service.with(customContext, () => {
|
|
throw new Error("Test error");
|
|
});
|
|
}).toThrow("Test error");
|
|
});
|
|
|
|
it("should return the function result", () => {
|
|
const customContext = context.active();
|
|
const result = service.with(customContext, () => {
|
|
return { data: "test", count: 42 };
|
|
});
|
|
|
|
expect(result).toEqual({ data: "test", count: 42 });
|
|
});
|
|
});
|
|
|
|
describe("withActiveSpan", () => {
|
|
it("should set span as active for function execution", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("test-span");
|
|
|
|
const result = service.withActiveSpan(span, () => {
|
|
const activeSpan = trace.getActiveSpan();
|
|
return activeSpan;
|
|
});
|
|
|
|
expect(result).toBe(span);
|
|
span.end();
|
|
});
|
|
|
|
it("should return the function result", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("test-span");
|
|
|
|
const result = service.withActiveSpan(span, () => {
|
|
return "executed-with-span";
|
|
});
|
|
|
|
expect(result).toBe("executed-with-span");
|
|
span.end();
|
|
});
|
|
|
|
it("should handle async operations", async () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("test-span");
|
|
|
|
const result = await service.withActiveSpan(span, async () => {
|
|
return new Promise((resolve) => {
|
|
setTimeout(() => resolve("async-result"), 10);
|
|
});
|
|
});
|
|
|
|
expect(result).toBe("async-result");
|
|
span.end();
|
|
});
|
|
|
|
it("should propagate exceptions", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("test-span");
|
|
|
|
expect(() => {
|
|
service.withActiveSpan(span, () => {
|
|
throw new Error("Span execution error");
|
|
});
|
|
}).toThrow("Span execution error");
|
|
|
|
span.end();
|
|
});
|
|
|
|
it("should nest spans correctly", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const parentSpan = tracer.startSpan("parent-span");
|
|
const childSpan = tracer.startSpan("child-span");
|
|
|
|
const result = service.withActiveSpan(parentSpan, () => {
|
|
return service.withActiveSpan(childSpan, () => {
|
|
const activeSpan = trace.getActiveSpan();
|
|
return activeSpan;
|
|
});
|
|
});
|
|
|
|
expect(result).toBe(childSpan);
|
|
childSpan.end();
|
|
parentSpan.end();
|
|
});
|
|
});
|
|
|
|
describe("integration scenarios", () => {
|
|
it("should support complex span context propagation", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span1 = tracer.startSpan("span-1");
|
|
const span2 = tracer.startSpan("span-2");
|
|
|
|
// Execute with span1 active
|
|
const ctx1 = trace.setSpan(context.active(), span1);
|
|
const result1 = service.with(ctx1, () => {
|
|
return service.getActiveSpan();
|
|
});
|
|
|
|
// Execute with span2 active
|
|
const ctx2 = trace.setSpan(context.active(), span2);
|
|
const result2 = service.with(ctx2, () => {
|
|
return service.getActiveSpan();
|
|
});
|
|
|
|
expect(result1).toBe(span1);
|
|
expect(result2).toBe(span2);
|
|
|
|
span1.end();
|
|
span2.end();
|
|
});
|
|
|
|
it("should handle context isolation", () => {
|
|
const tracer = trace.getTracer("test-tracer");
|
|
const span = tracer.startSpan("isolated-span");
|
|
|
|
// Execute with span active
|
|
service.withActiveSpan(span, () => {
|
|
const activeInside = service.getActiveSpan();
|
|
expect(activeInside).toBe(span);
|
|
});
|
|
|
|
// Outside the context, span should not be active
|
|
const activeOutside = service.getActiveSpan();
|
|
expect(activeOutside).not.toBe(span);
|
|
|
|
span.end();
|
|
});
|
|
});
|
|
});
|