UNPKG

autotel

Version:
409 lines (407 loc) 12.2 kB
import { configure } from "./config.js"; import { assertEventTracked, assertOutcomeTracked, createEventCollector } from "./event-testing.js"; import { SpanStatusCode, context, trace } from "@opentelemetry/api"; //#region src/testing.ts /** * Testing Utilities * * Helpers for testing instrumented code and verifying telemetry. * Perfect for integration tests and QA in production validation. * * @example Verify traces are created * ```typescript * import { assertTraceCreated, collectTraces } from '@your-org/otel-decorators/testing' * * describe('UserService', () => { * it('should create trace for user creation', async () => { * const collector = collectTraces() * * const service = new UserService() * await service.createUser({ email: 'test@example.com' }) * * assertTraceCreated(collector, 'user.createUser') * }) * }) * ``` */ /** * Create an in-memory trace collector for testing * * IMPORTANT: This automatically configures the global tracer to record spans. * Call this in your test's beforeEach() to ensure proper setup. * * @example * ```typescript * import { createTraceCollector } from 'autotel/testing' * * describe('MyService', () => { * let collector: TraceCollector * * beforeEach(() => { * collector = createTraceCollector() * }) * * it('should trace operations', async () => { * await myService.doSomething() * * const spans = collector.getSpansByName('myService.doSomething') * expect(spans).toHaveLength(1) * }) * }) * ``` */ function createTraceCollector() { const spans = []; const createMockSpan = (name, startTime) => { const spanData = { name, startTime, attributes: {}, status: { code: SpanStatusCode.OK } }; const spanContextData = { traceId: "1234567890abcdef1234567890abcdef", spanId: "1234567890abcdef", traceFlags: 1, isRemote: false }; return { spanContext: () => spanContextData, setStatus(status) { spanData.status = status; return this; }, setAttributes(attributes) { spanData.attributes = { ...spanData.attributes, ...attributes }; return this; }, setAttribute(key, value) { spanData.attributes = spanData.attributes || {}; spanData.attributes[key] = value; return this; }, addEvent(name, attributesOrStartTime, startTime) { return this; }, addLink(link) { return this; }, addLinks(links) { return this; }, updateName(newName) { spanData.name = newName; return this; }, isRecording() { return true; }, recordException(exception, time) {}, end(endTimeArg) { const endTime = performance.now(); spans.push({ name: spanData.name, status: spanData.status, attributes: spanData.attributes || {}, startTime: spanData.startTime, endTime, duration: endTime - spanData.startTime }); } }; }; configure({ tracer: { startSpan(name, options, ctx) { return createMockSpan(name, performance.now()); }, startActiveSpan(name, optionsOrFn, contextOrFn, fn) { const callback = (() => { if (typeof optionsOrFn === "function") return optionsOrFn; if (typeof contextOrFn === "function") return contextOrFn; if (fn) return fn; throw new Error("startActiveSpan requires a callback"); })(); const mockSpan = createMockSpan(name, performance.now()); const ctx = trace.setSpan(context.active(), mockSpan); return context.with(ctx, () => callback(mockSpan)); } } }); return { getSpans() { return [...spans]; }, getSpansByName(name) { return spans.filter((span) => span.name === name); }, getSpansByAttributes(attributes) { return spans.filter((span) => { return Object.entries(attributes).every(([key, value]) => span.attributes[key] === value); }); }, clear() { spans.length = 0; }, recordSpan(span) { spans.push(span); } }; } /** * Assert that a trace was created for an operation * * @param collector - Trace collector * @param operationName - Expected operation name * @param options - Optional assertion options * @throws Error if trace was not found or doesn't match expectations * * @example * ```typescript * assertTraceCreated(collector, 'user.createUser') * assertTraceCreated(collector, 'user.createUser', { * minCount: 1, * maxCount: 1, * status: SpanStatusCode.OK, * attributes: { 'user.email': 'test@example.com' } * }) * ``` */ function assertTraceCreated(collector, operationName, options) { const spans = collector.getSpansByName(operationName); if (options?.minCount !== void 0 && spans.length < options.minCount) throw new Error(`Expected at least ${options.minCount} traces for ${operationName}, got ${spans.length}`); if (options?.maxCount !== void 0 && spans.length > options.maxCount) throw new Error(`Expected at most ${options.maxCount} traces for ${operationName}, got ${spans.length}`); if (spans.length === 0) throw new Error(`No traces found for operation: ${operationName}`); if (options?.status !== void 0) { if (spans.filter((span) => span.status.code === options.status).length === 0) throw new Error(`No traces with status ${options.status} found for ${operationName}`); } if (options?.attributes) { if (spans.filter((span) => { return Object.entries(options.attributes).every(([key, value]) => span.attributes[key] === value); }).length === 0) throw new Error(`No traces with attributes ${JSON.stringify(options.attributes)} found for ${operationName}`); } } /** * Assert that no errors were logged * * Use this in smoke tests to verify critical paths don't have errors. * * @param collector - Trace collector * @throws Error if any error traces are found * * @example * ```typescript * // Run critical user flows * await runSmokeTests() * * // Verify no errors occurred * assertNoErrors(collector) * ``` */ function assertNoErrors(collector) { const errorSpans = collector.getSpans().filter((span) => span.status.code === SpanStatusCode.ERROR); if (errorSpans.length > 0) { const errorSummary = errorSpans.map((span) => `${span.name}: ${span.status.message}`).join("\n"); throw new Error(`Found ${errorSpans.length} error spans:\n${errorSummary}`); } } /** * Assert that a trace was created and succeeded * * @param collector - Trace collector * @param operationName - Expected operation name * * @example * ```typescript * assertTraceSucceeded(collector, 'user.createUser') * ``` */ function assertTraceSucceeded(collector, operationName) { assertTraceCreated(collector, operationName, { status: SpanStatusCode.OK }); } /** * Assert that a trace was created and failed * * @param collector - Trace collector * @param operationName - Expected operation name * @param errorMessage - Optional expected error message * * @example * ```typescript * assertTraceFailed(collector, 'user.createUser', 'Invalid email') * ``` */ function assertTraceFailed(collector, operationName, errorMessage) { const spans = collector.getSpansByName(operationName); if (spans.length === 0) throw new Error(`No traces found for operation: ${operationName}`); const errorSpans = spans.filter((span) => span.status.code === SpanStatusCode.ERROR); if (errorSpans.length === 0) throw new Error(`No error traces found for operation: ${operationName}`); if (errorMessage) { if (errorSpans.filter((span) => span.status.message === errorMessage).length === 0) throw new Error(`No error traces with message "${errorMessage}" found for ${operationName}`); } } /** * Create an in-memory log collector for testing * * @example * ```typescript * const logger = createMockLogger() * * // Use logger in your code * service.log = logger * await service.doSomething() * * // Assert logs were created * const logs = logger.getLogs() * expect(logs).toHaveLength(2) * expect(logs[0].message).toBe('Operation started') * ``` */ function createMockLogger() { const logs = []; const createLogMethod = (level) => { return (objOrMsg, msg) => { if (typeof objOrMsg === "string") logs.push({ level, message: objOrMsg, extra: void 0 }); else logs.push({ level, message: msg || "", extra: objOrMsg }); }; }; return { info: createLogMethod("info"), warn: createLogMethod("warn"), debug: createLogMethod("debug"), error(objOrMsg, msg) { if (typeof objOrMsg === "string") { logs.push({ level: "error", message: objOrMsg, extra: void 0, error: void 0 }); return; } const { err, ...rest } = objOrMsg; logs.push({ level: "error", message: msg || "", error: err instanceof Error ? err : void 0, extra: err !== void 0 && !(err instanceof Error) ? { err, ...rest } : rest }); }, getLogs() { return [...logs]; }, getLogsByLevel(level) { return logs.filter((log) => log.level === level); }, getLogsByMessage(message) { return logs.filter((log) => log.message.includes(message)); }, clear() { logs.length = 0; } }; } /** * Assert that no error logs were created * * @param logger - Log collector * @throws Error if any error logs are found * * @example * ```typescript * assertNoErrorsLogged(logger) * ``` */ function assertNoErrorsLogged(logger) { const errorLogs = logger.getLogsByLevel("error"); if (errorLogs.length > 0) { const errorSummary = errorLogs.map((log) => `${log.message}${log.error ? ": " + log.error.message : ""}`).join("\n"); throw new Error(`Found ${errorLogs.length} error logs:\n${errorSummary}`); } } /** * Wait for a specific trace to be created * * Useful for async operations where you need to wait for telemetry. * * @param collector - Trace collector * @param operationName - Expected operation name * @param timeoutMs - Timeout in milliseconds (default 5000) * @returns Promise that resolves when trace is found * @throws Error if timeout is reached * * @example * ```typescript * // Start async operation * const promise = service.doAsyncWork() * * // Wait for trace * await waitForTrace(collector, 'service.doAsyncWork', 1000) * * // Now you can assert on the trace * assertTraceSucceeded(collector, 'service.doAsyncWork') * ``` */ async function waitForTrace(collector, operationName, timeoutMs = 5e3) { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { if (collector.getSpansByName(operationName).length > 0) return; await new Promise((resolve) => setTimeout(resolve, 10)); } throw new Error(`Timeout waiting for trace ${operationName} after ${timeoutMs}ms`); } /** * Get trace duration in milliseconds * * @param collector - Trace collector * @param operationName - Operation name * @returns Duration in milliseconds, or undefined if trace not found * * @example * ```typescript * const duration = getTraceDuration(collector, 'user.createUser') * expect(duration).toBeLessThan(1000) // Should be < 1s * ``` */ function getTraceDuration(collector, operationName) { const spans = collector.getSpansByName(operationName); if (spans.length === 0) return; return spans[0]?.duration; } /** * Assert that an operation completed within a time threshold * * Perfect for performance testing and SLO validation. * * @param collector - Trace collector * @param operationName - Operation name * @param maxDurationMs - Maximum allowed duration in milliseconds * @throws Error if operation took too long * * @example * ```typescript * // Verify operation meets SLO * await service.createUser({ email: 'test@example.com' }) * assertTraceDuration(collector, 'user.createUser', 500) // Must be < 500ms * ``` */ function assertTraceDuration(collector, operationName, maxDurationMs) { const duration = getTraceDuration(collector, operationName); if (duration === void 0) throw new Error(`No trace found for operation: ${operationName}`); if (duration > maxDurationMs) throw new Error(`Operation ${operationName} took ${duration.toFixed(2)}ms, exceeding ${maxDurationMs}ms threshold`); } //#endregion export { assertEventTracked, assertNoErrors, assertNoErrorsLogged, assertOutcomeTracked, assertTraceCreated, assertTraceDuration, assertTraceFailed, assertTraceSucceeded, createEventCollector, createMockLogger, createTraceCollector, getTraceDuration, waitForTrace }; //# sourceMappingURL=testing.js.map