0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
import path from 'path';
|
|
|
|
import protobuf from 'protobufjs';
|
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
bytesToHex,
|
|
decodeExportTraceServiceRequest,
|
|
initializeProtobuf,
|
|
longToNumber,
|
|
} from '../../src/tracing/protobuf';
|
|
|
|
// Mock the logger
|
|
vi.mock('../../src/logger', () => ({
|
|
default: {
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Helper to create protobuf-encoded OTLP data
|
|
let protoRoot: protobuf.Root | null = null;
|
|
let ExportTraceServiceRequest: protobuf.Type | null = null;
|
|
|
|
async function getProtoRoot(): Promise<protobuf.Root> {
|
|
if (protoRoot) {
|
|
return protoRoot;
|
|
}
|
|
const protoDir = path.join(__dirname, '../../src/tracing/proto');
|
|
|
|
// Create a new Root with the proto directory as the include path
|
|
const root = new protobuf.Root();
|
|
root.resolvePath = (_origin: string, target: string) => {
|
|
return path.join(protoDir, target);
|
|
};
|
|
await root.load('opentelemetry/proto/collector/trace/v1/trace_service.proto');
|
|
|
|
protoRoot = root;
|
|
return protoRoot;
|
|
}
|
|
|
|
async function encodeOTLPRequest(data: any): Promise<Buffer> {
|
|
const root = await getProtoRoot();
|
|
if (!ExportTraceServiceRequest) {
|
|
ExportTraceServiceRequest = root.lookupType(
|
|
'opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest',
|
|
);
|
|
}
|
|
const message = ExportTraceServiceRequest.create(data);
|
|
const encoded = ExportTraceServiceRequest.encode(message).finish();
|
|
return Buffer.from(encoded);
|
|
}
|
|
|
|
describe('Protobuf decoding', () => {
|
|
beforeAll(async () => {
|
|
// Initialize proto definitions for faster subsequent tests
|
|
await initializeProtobuf();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.resetAllMocks();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('bytesToHex', () => {
|
|
it('should convert Uint8Array to hex string', () => {
|
|
const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
expect(bytesToHex(bytes, 8)).toBe('deadbeef');
|
|
});
|
|
|
|
it('should pad short hex strings to expected length', () => {
|
|
const bytes = new Uint8Array([0x00, 0x01]);
|
|
expect(bytesToHex(bytes, 8)).toBe('00000001');
|
|
});
|
|
|
|
it('should return zeros for undefined input', () => {
|
|
expect(bytesToHex(undefined, 8)).toBe('00000000');
|
|
});
|
|
|
|
it('should return zeros for empty array', () => {
|
|
expect(bytesToHex(new Uint8Array([]), 8)).toBe('00000000');
|
|
});
|
|
|
|
it('should handle full trace ID length', () => {
|
|
const bytes = new Uint8Array([
|
|
0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90, 0x12, 0x34, 0x56, 0x78, 0x90,
|
|
0x12,
|
|
]);
|
|
expect(bytesToHex(bytes, 32)).toBe('12345678901234567890123456789012');
|
|
});
|
|
});
|
|
|
|
describe('longToNumber', () => {
|
|
it('should return 0 for undefined', () => {
|
|
expect(longToNumber(undefined)).toBe(0);
|
|
});
|
|
|
|
it('should return number as-is', () => {
|
|
expect(longToNumber(12345)).toBe(12345);
|
|
});
|
|
|
|
it('should convert Long-like object to number', () => {
|
|
const longValue = {
|
|
low: 1000,
|
|
high: 0,
|
|
unsigned: false,
|
|
toNumber: () => 1000,
|
|
toString: () => '1000',
|
|
};
|
|
expect(longToNumber(longValue)).toBe(1000);
|
|
});
|
|
});
|
|
|
|
describe('decodeExportTraceServiceRequest', () => {
|
|
it('should decode a simple protobuf trace request', async () => {
|
|
const traceIdBytes = new Uint8Array([
|
|
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
|
0x99,
|
|
]);
|
|
const spanIdBytes = new Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
|
|
|
|
const testData = {
|
|
resourceSpans: [
|
|
{
|
|
resource: {
|
|
attributes: [{ key: 'service.name', value: { stringValue: 'test-service' } }],
|
|
},
|
|
scopeSpans: [
|
|
{
|
|
scope: {
|
|
name: 'test-tracer',
|
|
version: '1.0.0',
|
|
},
|
|
spans: [
|
|
{
|
|
traceId: traceIdBytes,
|
|
spanId: spanIdBytes,
|
|
name: 'test-span',
|
|
kind: 1,
|
|
startTimeUnixNano: 1700000000000000000n,
|
|
endTimeUnixNano: 1700000001000000000n,
|
|
status: {
|
|
code: 0,
|
|
message: 'OK',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const encoded = await encodeOTLPRequest(testData);
|
|
const decoded = await decodeExportTraceServiceRequest(encoded);
|
|
|
|
expect(decoded.resourceSpans).toHaveLength(1);
|
|
expect(decoded.resourceSpans[0].scopeSpans).toHaveLength(1);
|
|
expect(decoded.resourceSpans[0].scopeSpans[0].spans).toHaveLength(1);
|
|
|
|
const span = decoded.resourceSpans[0].scopeSpans[0].spans[0];
|
|
expect(span.name).toBe('test-span');
|
|
expect(span.kind).toBe(1);
|
|
});
|
|
|
|
it('should decode multiple spans', async () => {
|
|
const traceIdBytes = new Uint8Array([
|
|
0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd,
|
|
0xef,
|
|
]);
|
|
|
|
const testData = {
|
|
resourceSpans: [
|
|
{
|
|
scopeSpans: [
|
|
{
|
|
spans: [
|
|
{
|
|
traceId: traceIdBytes,
|
|
spanId: new Uint8Array([0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11]),
|
|
name: 'span-1',
|
|
startTimeUnixNano: 1000000000n,
|
|
},
|
|
{
|
|
traceId: traceIdBytes,
|
|
spanId: new Uint8Array([0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22]),
|
|
parentSpanId: new Uint8Array([0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11]),
|
|
name: 'span-2',
|
|
startTimeUnixNano: 2000000000n,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const encoded = await encodeOTLPRequest(testData);
|
|
const decoded = await decodeExportTraceServiceRequest(encoded);
|
|
|
|
const spans = decoded.resourceSpans[0].scopeSpans[0].spans;
|
|
expect(spans).toHaveLength(2);
|
|
expect(spans[0].name).toBe('span-1');
|
|
expect(spans[1].name).toBe('span-2');
|
|
});
|
|
|
|
it('should decode different attribute types', async () => {
|
|
const traceIdBytes = new Uint8Array([
|
|
0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
|
0x99,
|
|
]);
|
|
const spanIdBytes = new Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
|
|
|
|
const testData = {
|
|
resourceSpans: [
|
|
{
|
|
scopeSpans: [
|
|
{
|
|
spans: [
|
|
{
|
|
traceId: traceIdBytes,
|
|
spanId: spanIdBytes,
|
|
name: 'test-attributes',
|
|
startTimeUnixNano: 1000000000n,
|
|
attributes: [
|
|
{ key: 'string.attr', value: { stringValue: 'hello' } },
|
|
{ key: 'int.attr', value: { intValue: 123 } },
|
|
{ key: 'double.attr', value: { doubleValue: 3.14 } },
|
|
{ key: 'bool.attr', value: { boolValue: true } },
|
|
{
|
|
key: 'array.attr',
|
|
value: {
|
|
arrayValue: {
|
|
values: [{ stringValue: 'a' }, { stringValue: 'b' }],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const encoded = await encodeOTLPRequest(testData);
|
|
const decoded = await decodeExportTraceServiceRequest(encoded);
|
|
|
|
const span = decoded.resourceSpans[0].scopeSpans[0].spans[0];
|
|
expect(span.attributes).toBeDefined();
|
|
|
|
// Check that attributes are present
|
|
const attrs = span.attributes!;
|
|
expect(attrs.find((a) => a.key === 'string.attr')?.value?.stringValue).toBe('hello');
|
|
expect(attrs.find((a) => a.key === 'int.attr')?.value?.intValue).toBe(123);
|
|
expect(attrs.find((a) => a.key === 'double.attr')?.value?.doubleValue).toBeCloseTo(3.14);
|
|
expect(attrs.find((a) => a.key === 'bool.attr')?.value?.boolValue).toBe(true);
|
|
expect(attrs.find((a) => a.key === 'array.attr')?.value?.arrayValue?.values).toHaveLength(2);
|
|
});
|
|
|
|
it('should decode resource attributes', async () => {
|
|
const traceIdBytes = new Uint8Array([
|
|
0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd,
|
|
0xef,
|
|
]);
|
|
const spanIdBytes = new Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
|
|
|
|
const testData = {
|
|
resourceSpans: [
|
|
{
|
|
resource: {
|
|
attributes: [
|
|
{ key: 'service.name', value: { stringValue: 'my-service' } },
|
|
{ key: 'service.version', value: { stringValue: '2.0.0' } },
|
|
{ key: 'telemetry.sdk.name', value: { stringValue: 'opentelemetry' } },
|
|
],
|
|
},
|
|
scopeSpans: [
|
|
{
|
|
spans: [
|
|
{
|
|
traceId: traceIdBytes,
|
|
spanId: spanIdBytes,
|
|
name: 'test-span',
|
|
startTimeUnixNano: 1000000000n,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const encoded = await encodeOTLPRequest(testData);
|
|
const decoded = await decodeExportTraceServiceRequest(encoded);
|
|
|
|
const resourceAttrs = decoded.resourceSpans[0].resource?.attributes;
|
|
expect(resourceAttrs).toBeDefined();
|
|
expect(resourceAttrs!.find((a) => a.key === 'service.name')?.value?.stringValue).toBe(
|
|
'my-service',
|
|
);
|
|
expect(resourceAttrs!.find((a) => a.key === 'service.version')?.value?.stringValue).toBe(
|
|
'2.0.0',
|
|
);
|
|
});
|
|
|
|
it('should throw error for invalid protobuf data', async () => {
|
|
const invalidData = Buffer.from('not valid protobuf data');
|
|
|
|
await expect(decodeExportTraceServiceRequest(invalidData)).rejects.toThrow(
|
|
/invalid protobuf/i,
|
|
);
|
|
});
|
|
|
|
it('should handle empty resourceSpans', async () => {
|
|
const testData = {
|
|
resourceSpans: [],
|
|
};
|
|
|
|
const encoded = await encodeOTLPRequest(testData);
|
|
const decoded = await decodeExportTraceServiceRequest(encoded);
|
|
|
|
expect(decoded.resourceSpans).toEqual([]);
|
|
});
|
|
|
|
it('should handle Buffer input', async () => {
|
|
const traceIdBytes = new Uint8Array([
|
|
0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd,
|
|
0xef,
|
|
]);
|
|
const spanIdBytes = new Uint8Array([0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]);
|
|
|
|
const testData = {
|
|
resourceSpans: [
|
|
{
|
|
scopeSpans: [
|
|
{
|
|
spans: [
|
|
{
|
|
traceId: traceIdBytes,
|
|
spanId: spanIdBytes,
|
|
name: 'test-span',
|
|
startTimeUnixNano: 1000000000n,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const encoded = await encodeOTLPRequest(testData);
|
|
// Pass as Buffer instead of Uint8Array
|
|
const decoded = await decodeExportTraceServiceRequest(Buffer.from(encoded));
|
|
|
|
expect(decoded.resourceSpans).toHaveLength(1);
|
|
expect(decoded.resourceSpans[0].scopeSpans[0].spans[0].name).toBe('test-span');
|
|
});
|
|
});
|
|
});
|