mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-06 05:55:42 +00:00
160 lines
5.1 KiB
TypeScript
160 lines
5.1 KiB
TypeScript
import { SpanStatusCode, trace } from '@opentelemetry/api';
|
|
import {
|
|
BasicTracerProvider,
|
|
InMemorySpanExporter,
|
|
SimpleSpanProcessor,
|
|
} from '@opentelemetry/sdk-trace-base';
|
|
import { afterAll, beforeEach, describe, expect, it } from 'vitest';
|
|
import { Span, withSpan } from './span';
|
|
|
|
const exporter = new InMemorySpanExporter();
|
|
const provider = new BasicTracerProvider({
|
|
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
|
});
|
|
trace.setGlobalTracerProvider(provider);
|
|
|
|
afterAll(async () => {
|
|
await provider.shutdown();
|
|
trace.disable();
|
|
});
|
|
|
|
beforeEach(() => {
|
|
exporter.reset();
|
|
});
|
|
|
|
describe('withSpan', () => {
|
|
it('records a span around a sync function and returns its value', () => {
|
|
const result = withSpan('sync.op', { a: 1 }, () => 42);
|
|
expect(result).toBe(42);
|
|
|
|
const spans = exporter.getFinishedSpans();
|
|
expect(spans).toHaveLength(1);
|
|
expect(spans[0].name).toBe('sync.op');
|
|
expect(spans[0].attributes).toMatchObject({ a: 1 });
|
|
expect(spans[0].status.code).toBe(SpanStatusCode.OK);
|
|
});
|
|
|
|
it('records a span around an async function', async () => {
|
|
const result = await withSpan('async.op', {}, async () => 'ok');
|
|
expect(result).toBe('ok');
|
|
|
|
const spans = exporter.getFinishedSpans();
|
|
expect(spans).toHaveLength(1);
|
|
expect(spans[0].name).toBe('async.op');
|
|
expect(spans[0].status.code).toBe(SpanStatusCode.OK);
|
|
});
|
|
|
|
it('marks the span errored and rethrows on async rejection', async () => {
|
|
await expect(
|
|
withSpan('async.fail', {}, async () => {
|
|
throw new Error('boom');
|
|
}),
|
|
).rejects.toThrow('boom');
|
|
|
|
const spans = exporter.getFinishedSpans();
|
|
expect(spans).toHaveLength(1);
|
|
expect(spans[0].status.code).toBe(SpanStatusCode.ERROR);
|
|
expect(spans[0].events.some((e) => e.name === 'exception')).toBe(true);
|
|
});
|
|
|
|
it('resolves an attribute factory lazily', () => {
|
|
withSpan(
|
|
'factory.op',
|
|
() => ({ computed: 'yes' }),
|
|
() => null,
|
|
);
|
|
expect(exporter.getFinishedSpans()[0].attributes).toMatchObject({
|
|
computed: 'yes',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Span decorator', () => {
|
|
it('defaults the span name to Class.method', async () => {
|
|
class Widget {
|
|
@Span()
|
|
async fetch() {
|
|
return 'data';
|
|
}
|
|
}
|
|
expect(await new Widget().fetch()).toBe('data');
|
|
expect(exporter.getFinishedSpans()[0].name).toBe('Widget.fetch');
|
|
});
|
|
|
|
it('uses an explicit name and static attributes', async () => {
|
|
class Widget {
|
|
@Span('custom.name', { kind: 'static' })
|
|
async fetch() {
|
|
return 1;
|
|
}
|
|
}
|
|
await new Widget().fetch();
|
|
|
|
const [span] = exporter.getFinishedSpans();
|
|
expect(span.name).toBe('custom.name');
|
|
expect(span.attributes).toMatchObject({ kind: 'static' });
|
|
});
|
|
|
|
it('builds attributes from the call arguments via a factory', async () => {
|
|
class Db {
|
|
@Span('db.read', (query: string) => ({ 'db.statement': query }))
|
|
async read(query: string, _params: unknown[] = []) {
|
|
return [query];
|
|
}
|
|
}
|
|
await new Db().read('SELECT 1', []);
|
|
|
|
const [span] = exporter.getFinishedSpans();
|
|
expect(span.name).toBe('db.read');
|
|
expect(span.attributes).toMatchObject({ 'db.statement': 'SELECT 1' });
|
|
});
|
|
|
|
it('records errors thrown by the decorated method', async () => {
|
|
class Db {
|
|
@Span('db.read')
|
|
async read() {
|
|
throw new Error('nope');
|
|
}
|
|
}
|
|
await expect(new Db().read()).rejects.toThrow('nope');
|
|
expect(exporter.getFinishedSpans()[0].status.code).toBe(
|
|
SpanStatusCode.ERROR,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('withSpan — failure paths', () => {
|
|
it('records the error and rethrows when a sync function throws', () => {
|
|
expect(() =>
|
|
withSpan('sync.boom', {}, () => {
|
|
throw new Error('sync failure');
|
|
}),
|
|
).toThrow('sync failure');
|
|
|
|
const [span] = exporter.getFinishedSpans();
|
|
expect(span.name).toBe('sync.boom');
|
|
expect(span.status.code).toBe(SpanStatusCode.ERROR);
|
|
expect(span.status.message).toBe('sync failure');
|
|
expect(span.events.map((e) => e.name)).toContain('exception');
|
|
});
|
|
|
|
it('wraps a non-Error throw so the span still carries a message', async () => {
|
|
await expect(
|
|
withSpan('async.nonerror', {}, async () => {
|
|
throw 'just a string';
|
|
}),
|
|
).rejects.toBe('just a string');
|
|
|
|
const [span] = exporter.getFinishedSpans();
|
|
expect(span.status.code).toBe(SpanStatusCode.ERROR);
|
|
expect(span.status.message).toBe('just a string');
|
|
});
|
|
|
|
it('mirrors the span name into a filterable attribute', () => {
|
|
withSpan('named.op', {}, () => undefined);
|
|
expect(exporter.getFinishedSpans()[0].attributes).toMatchObject({
|
|
span_name: 'named.op',
|
|
});
|
|
});
|
|
});
|