UNPKG

autotel-cloudflare

Version:

The #1 OpenTelemetry package for Cloudflare Workers - complete bindings coverage, native CF OTel integration, advanced sampling

579 lines (480 loc) 17.2 kB
import { describe, it, expect, vi } from 'vitest'; import { instrument } from './instrument'; import { trace } from 'autotel-edge'; import type { ExportedHandler } from '@cloudflare/workers-types'; describe('Handler Instrumentation - Integration Tests', () => { interface Env { OTLP_ENDPOINT?: string; API_KEY?: string; } describe('instrument() for Fetch Handlers', () => { it('should instrument a basic fetch handler', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Hello World', { status: 200 }); }, }; const instrumented = instrument(handler, (env: Env) => ({ service: { name: 'test-worker', version: '1.0.0', }, exporter: { url: env.OTLP_ENDPOINT || 'http://localhost:4318/v1/traces', headers: { 'x-api-key': env.API_KEY || 'test-key' }, }, })); expect(instrumented).toBeDefined(); expect(typeof instrumented.fetch).toBe('function'); }); it('should create a new handler that wraps the original', async () => { let handlerCalled = false; const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { handlerCalled = true; return new Response('Hello World', { status: 200 }); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/test'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.fetch(request, env, ctx); expect(handlerCalled).toBe(true); }); it('should call waitUntil to flush spans', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Hello World', { status: 200 }); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/test'); const env = {} as Env; const waitUntilSpy = vi.fn(); const ctx = { waitUntil: waitUntilSpy, passThroughOnException: vi.fn(), } as any; await instrumented.fetch(request, env, ctx); // waitUntil should be called to flush spans expect(waitUntilSpy).toHaveBeenCalled(); }); it('should preserve handler errors', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { throw new Error('Handler error'); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/test'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await expect(instrumented.fetch(request, env, ctx)).rejects.toThrow('Handler error'); }); it('should work with trace() functions inside handler', async () => { const createUser = trace(async function createUser(email: string) { return { id: '123', email }; }); const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { const user = await createUser('test@example.com'); return Response.json(user); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/users'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; const response = await instrumented.fetch(request, env, ctx); const data = await response.json(); expect(data).toEqual({ id: '123', email: 'test@example.com' }); }); it('should accept static config object', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Hello', { status: 200 }); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker', version: '1.0.0', namespace: 'testing', }, exporter: { url: 'http://localhost:4318/v1/traces', }, }); expect(instrumented).toBeDefined(); expect(typeof instrumented.fetch).toBe('function'); }); it('should accept config function', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Hello', { status: 200 }); }, }; const instrumented = instrument(handler, (env: Env) => ({ service: { name: env.OTLP_ENDPOINT ? 'prod-worker' : 'dev-worker' }, })); expect(instrumented).toBeDefined(); expect(typeof instrumented.fetch).toBe('function'); }); }); describe('Handler with Scheduled Events', () => { it('should instrument scheduled handler', async () => { let scheduledCalled = false; const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Hello', { status: 200 }); }, async scheduled(event, env, ctx) { scheduledCalled = true; }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); expect(instrumented.scheduled).toBeDefined(); if (instrumented.scheduled) { const event = { scheduledTime: Date.now(), cron: '0 0 * * *' } as ScheduledController; const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.scheduled(event, env, ctx); expect(scheduledCalled).toBe(true); expect(ctx.waitUntil).toHaveBeenCalled(); } }); }); describe('Handler with Queue Events', () => { it('should instrument queue handler', async () => { let queueCalled = false; let messageCount = 0; const handler: ExportedHandler<Env> = { async queue(batch, env, ctx) { queueCalled = true; messageCount = batch.messages.length; }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); expect(instrumented.queue).toBeDefined(); if (instrumented.queue) { const batch = { messages: [ { id: '1', body: 'message1', timestamp: new Date() }, { id: '2', body: 'message2', timestamp: new Date() }, ], queue: 'test-queue', ackAll: vi.fn(), retryAll: vi.fn(), } as MessageBatch; const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.queue(batch, env, ctx); expect(queueCalled).toBe(true); expect(messageCount).toBe(2); expect(ctx.waitUntil).toHaveBeenCalled(); } }); it('should track message ack operations', async () => { const handler: ExportedHandler<Env> = { async queue(batch, env, ctx) { // Ack individual messages batch.messages[0].ack(); batch.messages[1].ack(); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); if (instrumented.queue) { const messages = [ { id: '1', body: 'message1', timestamp: new Date(), ack: vi.fn(), retry: vi.fn() }, { id: '2', body: 'message2', timestamp: new Date(), ack: vi.fn(), retry: vi.fn() }, ]; const batch = { messages, queue: 'test-queue', ackAll: vi.fn(), retryAll: vi.fn(), } as MessageBatch; const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.queue(batch, env, ctx); // Verify ack was called on both messages expect(messages[0].ack).toHaveBeenCalled(); expect(messages[1].ack).toHaveBeenCalled(); } }); it('should track message retry operations', async () => { const handler: ExportedHandler<Env> = { async queue(batch, env, ctx) { // Retry a message batch.messages[0].retry(); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); if (instrumented.queue) { const messages = [ { id: '1', body: 'message1', timestamp: new Date(), ack: vi.fn(), retry: vi.fn() }, ]; const batch = { messages, queue: 'test-queue', ackAll: vi.fn(), retryAll: vi.fn(), } as MessageBatch; const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.queue(batch, env, ctx); // Verify retry was called expect(messages[0].retry).toHaveBeenCalled(); } }); it('should track ackAll operation', async () => { const handler: ExportedHandler<Env> = { async queue(batch, env, ctx) { batch.ackAll(); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); if (instrumented.queue) { const ackAllFn = vi.fn(); const batch = { messages: [ { id: '1', body: 'message1', timestamp: new Date() }, { id: '2', body: 'message2', timestamp: new Date() }, ], queue: 'test-queue', ackAll: ackAllFn, retryAll: vi.fn(), } as MessageBatch; const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.queue(batch, env, ctx); expect(ackAllFn).toHaveBeenCalled(); } }); }); describe('Handler with Email Events', () => { it('should instrument email handler', async () => { let emailCalled = false; const handler: ExportedHandler<Env> = { async email(message, env, ctx) { emailCalled = true; }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); expect(instrumented.email).toBeDefined(); if (instrumented.email) { const message = { from: 'sender@example.com', to: 'recipient@example.com', headers: new Headers({ subject: 'Test Email' }), } as ForwardableEmailMessage; const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.email(message, env, ctx); expect(emailCalled).toBe(true); expect(ctx.waitUntil).toHaveBeenCalled(); } }); }); describe('Context Propagation', () => { it('should propagate trace context from request headers', async () => { const incomingTraceId = '0af7651916cd43dd8448eb211c80319c'; const incomingSpanId = 'b7ad6b7169203331'; let capturedTraceId: string | undefined; let capturedParentSpanId: string | undefined; const handler: ExportedHandler<Env> = { async fetch(_request, _env, _ctx) { const { trace: traceApi, context: contextApi } = await import('@opentelemetry/api'); const span = traceApi.getActiveSpan(); capturedTraceId = span?.spanContext().traceId; capturedParentSpanId = (span as any)?.parentSpanId; return new Response('OK', { status: 200 }); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/test', { headers: { 'traceparent': `00-${incomingTraceId}-${incomingSpanId}-01`, }, }); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; await instrumented.fetch(request, env, ctx); // The server span must inherit the traceId from the incoming traceparent header expect(capturedTraceId).toBe(incomingTraceId); // The server span's parent must be the incoming spanId expect(capturedParentSpanId).toBe(incomingSpanId); }); }); describe('Fetch handler span status based on HTTP status code', () => { it('should set OK status for 2xx responses', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('OK', { status: 200 }); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/test'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; const response = await instrumented.fetch(request, env, ctx); expect(response.status).toBe(200); }); it('should set OK status for 404 responses (client error, not server fault)', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Not Found', { status: 404 }); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/missing'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; const response = await instrumented.fetch(request, env, ctx); expect(response.status).toBe(404); }); it('should return 500 responses correctly (span ERROR status applied internally)', async () => { const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Internal Server Error', { status: 500 }); }, }; const instrumented = instrument(handler, { service: { name: 'test-worker' }, }); const request = new Request('http://example.com/fail'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; const response = await instrumented.fetch(request, env, ctx); expect(response.status).toBe(500); }); it('should still call postProcess after setting span status', async () => { let postProcessCalled = false; let postProcessResponse: Response | undefined; const handler: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Server Error', { status: 503 }); }, }; const instrumented = instrument(handler, (env: Env) => ({ service: { name: 'test-worker' }, handlers: { fetch: { postProcess(span, { response }) { postProcessCalled = true; postProcessResponse = response; }, }, }, })); const request = new Request('http://example.com/fail'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; const response = await instrumented.fetch(request, env, ctx); expect(response.status).toBe(503); expect(postProcessCalled).toBe(true); expect(postProcessResponse?.status).toBe(503); }); }); describe('Multiple Handlers', () => { it('should instrument multiple handlers independently', async () => { const handler1: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Handler 1', { status: 200 }); }, }; const handler2: ExportedHandler<Env> = { async fetch(request, env, ctx) { return new Response('Handler 2', { status: 200 }); }, }; const instrumented1 = instrument(handler1, { service: { name: 'worker-1' }, }); const instrumented2 = instrument(handler2, { service: { name: 'worker-2' }, }); const request = new Request('http://example.com/test'); const env = {} as Env; const ctx = { waitUntil: vi.fn(), passThroughOnException: vi.fn(), } as any; const response1 = await instrumented1.fetch(request, env, ctx); const response2 = await instrumented2.fetch(request, env, ctx); const text1 = await response1.text(); const text2 = await response2.text(); expect(text1).toBe('Handler 1'); expect(text2).toBe('Handler 2'); }); }); });