autotel
Version:
Write Once, Observe Anywhere
312 lines (253 loc) • 9.63 kB
text/typescript
/**
* Tests for semantic convention helpers
*/
import { describe, it, expect, beforeEach } from 'vitest';
import {
traceLLM,
traceDB,
traceHTTP,
traceMessaging,
} from './semantic-helpers';
import { createTraceCollector } from './testing';
describe('Semantic Helpers', () => {
let collector: ReturnType<typeof createTraceCollector>;
beforeEach(() => {
collector = createTraceCollector();
});
describe('traceLLM', () => {
it('should add Gen AI semantic convention attributes', async () => {
const generateText = traceLLM({
model: 'gpt-4',
operation: 'chat',
provider: 'openai',
})((_ctx) => async (prompt: string) => {
return `Response to: ${prompt}`;
});
await generateText('Hello');
const spans = collector.getSpans();
expect(spans).toHaveLength(1);
const span = spans[0];
expect(span.attributes['gen.ai.request.model']).toBe('gpt-4');
expect(span.attributes['gen.ai.operation.name']).toBe('chat');
expect(span.attributes['gen.ai.system']).toBe('openai');
});
it('should use default operation when not specified', async () => {
const generateText = traceLLM({
model: 'claude-3',
})((_ctx) => async () => 'result');
await generateText();
const spans = collector.getSpans();
expect(spans[0].attributes['gen.ai.operation.name']).toBe('chat');
});
it('should support embedding operation', async () => {
const embed = traceLLM({
model: 'text-embedding-3-small',
operation: 'embedding',
provider: 'openai',
})((_ctx) => async (_text: string) => [0.1, 0.2, 0.3]);
await embed('test text');
const spans = collector.getSpans();
expect(spans[0].attributes['gen.ai.operation.name']).toBe('embedding');
});
it('should support additional custom attributes', async () => {
const generateText = traceLLM({
model: 'gpt-4',
attributes: {
'custom.attribute': 'custom-value',
'custom.number': 123,
},
})((_ctx) => async () => 'result');
await generateText();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['gen.ai.request.model']).toBe('gpt-4');
expect(span.attributes['custom.attribute']).toBe('custom-value');
expect(span.attributes['custom.number']).toBe(123);
});
});
describe('traceDB', () => {
it('should add DB semantic convention attributes', async () => {
const getUser = traceDB({
system: 'postgresql',
operation: 'SELECT',
database: 'app_db',
collection: 'users',
})((_ctx) => async (userId: string) => {
return { id: userId, name: 'John' };
});
await getUser('123');
const spans = collector.getSpans();
expect(spans).toHaveLength(1);
const span = spans[0];
expect(span.attributes['db.system']).toBe('postgresql');
expect(span.attributes['db.operation']).toBe('SELECT');
expect(span.attributes['db.name']).toBe('app_db');
expect(span.attributes['db.collection.name']).toBe('users');
});
it('should work without optional attributes', async () => {
const query = traceDB({
system: 'mongodb',
})((_ctx) => async () => ({ results: [] }));
await query();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['db.system']).toBe('mongodb');
expect(span.attributes['db.operation']).toBeUndefined();
expect(span.attributes['db.name']).toBeUndefined();
});
it('should support custom attributes', async () => {
const query = traceDB({
system: 'redis',
operation: 'GET',
attributes: {
'db.redis.ttl': 3600,
},
})((_ctx) => async (key: string) => `value-${key}`);
await query('test-key');
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['db.system']).toBe('redis');
expect(span.attributes['db.redis.ttl']).toBe(3600);
});
});
describe('traceHTTP', () => {
it('should add HTTP semantic convention attributes', async () => {
const fetchUser = traceHTTP({
method: 'GET',
url: 'https://api.example.com/users/:id',
})((_ctx) => async (userId: string) => {
return { id: userId };
});
await fetchUser('123');
const spans = collector.getSpans();
expect(spans).toHaveLength(1);
const span = spans[0];
expect(span.attributes['http.request.method']).toBe('GET');
expect(span.attributes['url.full']).toBe(
'https://api.example.com/users/:id',
);
});
it('should work with only method', async () => {
const request = traceHTTP({
method: 'POST',
})((_ctx) => async (_data: object) => ({ success: true }));
await request({ test: 'data' });
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['http.request.method']).toBe('POST');
expect(span.attributes['url.full']).toBeUndefined();
});
it('should work with only URL', async () => {
const request = traceHTTP({
url: 'https://api.example.com',
})((_ctx) => async () => ({ success: true }));
await request();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['url.full']).toBe('https://api.example.com');
expect(span.attributes['http.request.method']).toBeUndefined();
});
it('should support custom attributes', async () => {
const request = traceHTTP({
method: 'POST',
url: 'https://webhook.example.com',
attributes: {
'http.request.retry_count': 3,
'http.request.timeout': 5000,
},
})((_ctx) => async () => ({ success: true }));
await request();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['http.request.retry_count']).toBe(3);
expect(span.attributes['http.request.timeout']).toBe(5000);
});
});
describe('traceMessaging', () => {
it('should add Messaging semantic convention attributes', async () => {
const publishEvent = traceMessaging({
system: 'kafka',
operation: 'publish',
destination: 'user-events',
})((_ctx) => async (_event: object) => {
return { messageId: '123' };
});
await publishEvent({ type: 'user.created' });
const spans = collector.getSpans();
expect(spans).toHaveLength(1);
const span = spans[0];
expect(span.attributes['messaging.system']).toBe('kafka');
expect(span.attributes['messaging.operation']).toBe('publish');
expect(span.attributes['messaging.destination.name']).toBe('user-events');
});
it('should work with minimal config', async () => {
const sendMessage = traceMessaging({
system: 'rabbitmq',
})((_ctx) => async () => ({ sent: true }));
await sendMessage();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['messaging.system']).toBe('rabbitmq');
expect(span.attributes['messaging.operation']).toBeUndefined();
expect(span.attributes['messaging.destination.name']).toBeUndefined();
});
it('should support receive operation', async () => {
const consumeMessage = traceMessaging({
system: 'sqs',
operation: 'receive',
destination: 'notifications',
})((_ctx) => async () => ({ messages: [] }));
await consumeMessage();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['messaging.operation']).toBe('receive');
});
it('should support custom attributes', async () => {
const publishBatch = traceMessaging({
system: 'aws_sqs',
operation: 'publish',
destination: 'orders',
attributes: {
'messaging.batch.message_count': 10,
'messaging.kafka.partition': 0,
},
})((_ctx) => async () => ({ success: true }));
await publishBatch();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['messaging.batch.message_count']).toBe(10);
expect(span.attributes['messaging.kafka.partition']).toBe(0);
});
});
describe('Attribute merging', () => {
it('should merge custom attributes with semantic attributes in traceLLM', async () => {
const fn = traceLLM({
model: 'gpt-4',
attributes: {
'gen.ai.request.temperature': 0.7,
'custom.attr': 'value',
},
})((_ctx) => async () => 'result');
await fn();
const spans = collector.getSpans();
const span = spans[0];
expect(span.attributes['gen.ai.request.model']).toBe('gpt-4');
expect(span.attributes['gen.ai.request.temperature']).toBe(0.7);
expect(span.attributes['custom.attr']).toBe('value');
});
it('should allow custom attributes to override semantic defaults', async () => {
const fn = traceDB({
system: 'postgresql',
operation: 'SELECT',
attributes: {
'db.operation': 'CUSTOM_OPERATION', // Override default
},
})((_ctx) => async () => ({ rows: [] }));
await fn();
const spans = collector.getSpans();
const span = spans[0];
// Custom attribute should win
expect(span.attributes['db.operation']).toBe('CUSTOM_OPERATION');
});
});
});