UNPKG

autotel-cloudflare

Version:

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

525 lines (426 loc) 15.1 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { instrumentDO } from './durable-objects'; import { trace, SpanStatusCode, SpanKind } from '@opentelemetry/api'; import type { DurableObjectState } from '@cloudflare/workers-types'; describe('Durable Objects Instrumentation', () => { let mockTracer: any; let mockSpan: any; let getTracerSpy: any; beforeEach(() => { mockSpan = { spanContext: () => ({ traceId: 'test-trace-id', spanId: 'test-span-id', traceFlags: 1, }), setAttribute: vi.fn(), setAttributes: vi.fn(), setStatus: vi.fn(), recordException: vi.fn(), end: vi.fn(), isRecording: () => true, updateName: vi.fn(), addEvent: vi.fn(), }; mockTracer = { startActiveSpan: vi.fn((name, options, context, fn) => { if (typeof options === 'function') { return options(mockSpan); } if (typeof context === 'function') { return context(mockSpan); } if (typeof fn === 'function') { return fn(mockSpan); } return Promise.resolve(); }), }; getTracerSpy = vi.spyOn(trace, 'getTracer').mockReturnValue(mockTracer as any); }); afterEach(() => { getTracerSpy.mockRestore(); }); describe('instrumentDO()', () => { it('should wrap DO class constructor', () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('Hello'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); expect(InstrumentedDO).toBeDefined(); expect(typeof InstrumentedDO).toBe('function'); }); it('should create DO instance with instrumentation', () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('Hello'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id-123', name: 'test-do-name', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const mockEnv = {}; const instance = new InstrumentedDO(mockState, mockEnv); expect(instance).toBeDefined(); expect(typeof instance.fetch).toBe('function'); }); it('should accept static config', () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('Hello'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do', version: '1.0.0' }, exporter: { url: 'http://localhost:4318/v1/traces', }, }); expect(InstrumentedDO).toBeDefined(); }); it('should accept config function', () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('Hello'); } } interface Env { OTLP_ENDPOINT: string; } const InstrumentedDO = instrumentDO(TestDO, (env: Env) => ({ service: { name: 'test-do' }, exporter: { url: env.OTLP_ENDPOINT, }, })); expect(InstrumentedDO).toBeDefined(); }); }); describe('fetch() instrumentation', () => { it('should create span for fetch() calls', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('Hello from DO'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id-123', name: 'test-do-name', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); const request = new Request('http://example.com/test'); const response = await instance.fetch(request); expect(mockTracer.startActiveSpan).toHaveBeenCalled(); const spanName = mockTracer.startActiveSpan.mock.calls[0][0]; expect(spanName).toContain('DO'); expect(spanName).toContain('test-do-name'); expect(spanName).toContain('GET'); const text = await response.text(); expect(text).toBe('Hello from DO'); }); it('should add HTTP attributes (method, URL, status)', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('OK', { status: 200 }); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id-123', name: 'counter', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); const request = new Request('http://example.com/increment', { method: 'POST' }); await instance.fetch(request); const options = mockTracer.startActiveSpan.mock.calls[0][1]; expect(options.kind).toBe(SpanKind.SERVER); expect(options.attributes['http.request.method']).toBe('POST'); expect(options.attributes['url.full']).toBe('http://example.com/increment'); }); it('should add DO-specific attributes (do.id, do.id.name)', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('OK'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'unique-do-id-456', name: 'my-counter', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); const request = new Request('http://example.com/test'); await instance.fetch(request); const options = mockTracer.startActiveSpan.mock.calls[0][1]; expect(options.attributes['do.id']).toBe('unique-do-id-456'); expect(options.attributes['do.id.name']).toBe('my-counter'); expect(options.attributes['faas.trigger']).toBe('http'); }); it('should track cold starts (first call = true, subsequent = false)', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('OK'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id', name: 'test', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); const request = new Request('http://example.com/test'); // First call - should be cold start await instance.fetch(request); const firstCallOptions = mockTracer.startActiveSpan.mock.calls[0][1]; expect(firstCallOptions.attributes['faas.coldstart']).toBe(true); // Second call - should NOT be cold start await instance.fetch(request); const secondCallOptions = mockTracer.startActiveSpan.mock.calls[1][1]; expect(secondCallOptions.attributes['faas.coldstart']).toBe(false); }); it('should handle fetch() errors', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { throw new Error('DO error'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id', name: 'test', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); const request = new Request('http://example.com/test'); await expect(instance.fetch(request)).rejects.toThrow('DO error'); expect(mockSpan.recordException).toHaveBeenCalled(); expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR, message: 'DO error', }); expect(mockSpan.end).toHaveBeenCalled(); }); it('should preserve "this" binding', async () => { class TestDO { private counter = 0; constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { this.counter++; return new Response(`Count: ${this.counter}`); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id', name: 'test', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); const response1 = await instance.fetch(new Request('http://example.com/test')); const text1 = await response1.text(); expect(text1).toBe('Count: 1'); const response2 = await instance.fetch(new Request('http://example.com/test')); const text2 = await response2.text(); expect(text2).toBe('Count: 2'); }); }); describe('alarm() instrumentation', () => { it('should create span for alarm() calls', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('OK'); } async alarm() { // Alarm logic } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id-alarm', name: 'alarm-do', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); await instance.alarm(); expect(mockTracer.startActiveSpan).toHaveBeenCalled(); const spanName = mockTracer.startActiveSpan.mock.calls[0][0]; expect(spanName).toContain('DO'); expect(spanName).toContain('alarm-do'); expect(spanName).toContain('alarm'); }); it('should add DO-specific attributes for alarm()', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('OK'); } async alarm() { // Alarm logic } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'alarm-id-123', name: 'cleanup-do', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); await instance.alarm(); const options = mockTracer.startActiveSpan.mock.calls[0][1]; expect(options.kind).toBe(SpanKind.INTERNAL); expect(options.attributes['do.id']).toBe('alarm-id-123'); expect(options.attributes['do.id.name']).toBe('cleanup-do'); expect(options.attributes['faas.trigger']).toBe('timer'); }); it('should track cold starts for alarm()', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('OK'); } async alarm() { // Alarm logic } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id', name: 'test', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); // First alarm call - should be cold start await instance.alarm(); const firstCallOptions = mockTracer.startActiveSpan.mock.calls[0][1]; expect(firstCallOptions.attributes['faas.coldstart']).toBe(true); // Second alarm call - should NOT be cold start await instance.alarm(); const secondCallOptions = mockTracer.startActiveSpan.mock.calls[1][1]; expect(secondCallOptions.attributes['faas.coldstart']).toBe(false); }); it('should handle alarm() errors', async () => { class TestDO { constructor(public state: DurableObjectState, public env: any) {} async fetch(request: Request) { return new Response('OK'); } async alarm() { throw new Error('Alarm failed'); } } const InstrumentedDO = instrumentDO(TestDO, { service: { name: 'test-do' }, }); const mockState = { id: { toString: () => 'test-id', name: 'test', equals: () => false, }, storage: {}, blockConcurrencyWhile: vi.fn(), waitUntil: vi.fn(), } as unknown as DurableObjectState; const instance = new InstrumentedDO(mockState, {}); await expect(instance.alarm()).rejects.toThrow('Alarm failed'); expect(mockSpan.recordException).toHaveBeenCalled(); expect(mockSpan.setStatus).toHaveBeenCalledWith({ code: SpanStatusCode.ERROR, message: 'Alarm failed', }); expect(mockSpan.end).toHaveBeenCalled(); }); }); });