Skip to content

Commit da31402

Browse files
authored
Add OTLP UDP Span Exporter for Lambda (#38)
#### Description Implementing a UDP exporter for OTLP spans in AWS Lambda environment. - If AppSignals is enabled in Lambda, the OTel spans will be exported via this UdpExporter. Currently it can't be configured to use any other exporter. - OTel spans is first protobuf encoded, then base64 encoded, then prefixed with `T1`, and finally appended to the `'{"format":"json","version":1}\n'` string before being exported to the address provided in the `AWS_XRAY_DAEMON_ADDRESS` or default `127.0.0.1:2000`. #### Testing Mimic Lambda environment in my local desktop by 1. setting up `AWS_LAMBDA_FUNCTION_NAME` and `AWS_XRAY_DAEMON_ADDRESS`. 2. run NodeJs Express webapp in this repo with above setting 3. install X-Ray Daemon to ingest UDP OTel span and send to XRay backend. **Printed spans in JSON format** Raw spans: ```json { "Id": "1-66d0a1c7-2add3ba009c719881b989671", "Duration": 0.118, "LimitExceeded": false, "Segments": [ { "Id": "1d98ad0be7483459", "Document": { "id": "1d98ad0be7483459", "name": "GET /http", "start_time": 1724948935.8739998, "trace_id": "1-66d0a1c7-2add3ba009c719881b989671", "end_time": 1724948935.9921796, "aws": { "span.kind": "LOCAL_ROOT", "local.service": "MY_APPLICATION", "local.operation": "GET /http", "is.local.root": true }, "annotations": { "span.kind": "SERVER" }, "metadata": { "net.host.name": "localhost", "net.transport": "ip_tcp", "net.peer.port": 53485, "http.target": "/http", "http.flavor": "1.1", "http.url": "http://localhost:8080/http", "net.peer.ip": "::1", "http.host": "localhost:8080", "net.host.ip": "::1", "http.status_code": 200, "http.user_agent": "curl/8.7.1", "net.host.port": 8080, "http.route": "/http", "http.status_text": "OK", "http.method": "GET", "http.scheme": "http" }, "subsegments": [ { "id": "b274383e8580e321", "name": "request handler - /http", "start_time": 1724948935.88, "end_time": 1724948935.8800387, "aws": { "local.operation": "GET /http", "is.local.root": false }, "annotations": { "span.kind": "INTERNAL" }, "metadata": { "http.route": "/http", "express.name": "/http", "express.type": "request_handler" } }, { "id": "0a52b918a72c8f2a", "name": "middleware - expressInit", "start_time": 1724948935.8739998, "end_time": 1724948935.8740563, "aws": { "local.operation": "GET /http", "is.local.root": false }, "annotations": { "span.kind": "INTERNAL" }, "metadata": { "http.route": "/", "express.name": "expressInit", "express.type": "middleware" } }, { "id": "be44b558387c8b3b", "name": "middleware - query", "start_time": 1724948935.8739998, "end_time": 1724948935.8740451, "aws": { "local.operation": "GET /http", "is.local.root": false }, "annotations": { "span.kind": "INTERNAL" }, "metadata": { "http.route": "/", "express.name": "query", "express.type": "middleware" } }, { "id": "e499eba67383bc25", "name": "GET", "start_time": 1724948935.887, "end_time": 1724948935.9880683, "aws": { "remote.operation": "GET /api", "span.kind": "CLIENT", "local.service": "MY_APPLICATION", "local.operation": "GET /http", "is.local.root": false, "remote.service": "www.randomnumberapi.com:80" }, "annotations": { "span.kind": "CLIENT" }, "metadata": { "net.transport": "ip_tcp", "net.peer.name": "www.randomnumberapi.com", "http.status_code": 200, "net.peer.port": 80, "http.target": "/api/v1.0/random", "http.flavor": "1.1", "http.response_content_length_uncompressed": 5, "http.url": "http://www.randomnumberapi.com/api/v1.0/random", "http.status_text": "OK", "net.peer.ip": "172.67.146.15", "http.method": "GET", "http.host": "www.randomnumberapi.com:80" }, "namespace": "remote" } ] } }, { "Id": "0b5b52a92b84df1a", "Document": { "id": "0b5b52a92b84df1a", "name": "GET", "start_time": 1724948935.887, "trace_id": "1-66d0a1c7-2add3ba009c719881b989671", "end_time": 1724948935.9880683, "parent_id": "e499eba67383bc25", "inferred": true } } ] } ``` Encoded trace data from SDK: ``` Lambda JS UDP exporter export Sending UDP data: {"format":"json","version":1} T1CogNCskHCiAKDHNlcnZpY2UubmFtZRIQCg5NWV9BUFBMSUNBVElPTgoiChZ0ZWxlbWV0cnkuc2RrLmxhbmd1YWdlEggKBm5vZGVqcwolChJ0ZWxlbWV0cnkuc2RrLm5hbWUSDwoNb3BlbnRlbGVtZXRyeQohChV0ZWxlbWV0cnkuc2RrLnZlcnNpb24SCAoGMS4yNS4xCiUKFnRlbGVtZXRyeS5hdXRvLnZlcnNpb24SCwoJMC4wLjEtYXdzChIKC3Byb2Nlc3MucGlkEgMYjVsKIQoXcHJvY2Vzcy5leGVjdXRhYmxlLm5hbWUSBgoEbm9kZQpDChdwcm9jZXNzLmV4ZWN1dGFibGUucGF0aBIoCiYvdXNyL2xvY2FsL0NlbGxhci9ub2RlLzIxLjYuMS9iaW4vbm9kZQqjAgoUcHJvY2Vzcy5jb21tYW5kX2FyZ3MSigIqhwIKKAomL3Vzci9sb2NhbC9DZWxsYXIvbm9kZS8yMS42LjEvYmluL25vZGUKCwoJLS1yZXF1aXJlCkEKP0Bhd3MvYXdzLWRpc3Ryby1vcGVudGVsZW1ldHJ5LW5vZGUtYXV0b2luc3RydW1lbnRhdGlvbi9yZWdpc3RlcgqKAQqHAS9Vc2Vycy94aWFtaS9Eb2N1bWVudHMvd29ya3NwYWNlL2FwbS9hd3Mtb3RlbC1qcy1pbnN0cnVtZW50YXRpb24vc2FtcGxlLWFwcGxpY2F0aW9ucy9zaW1wbGUtZXhwcmVzcy1zZXJ2ZXIvc2FtcGxlLWFwcC1leHByZXNzLXNlcnZlci5qcwojChdwcm9jZXNzLnJ1bnRpbWUudmVyc2lvbhIICgYyMS42LjEKIAoUcHJvY2Vzcy5ydW50aW1lLm5hbWUSCAoGbm9kZWpzCigKG3Byb2Nlc3MucnVudGltZS5kZXNjcmlwdGlvbhIJCgdOb2RlLmpzCp4BCg9wcm9jZXNzLmNvbW1hbmQSigEKhwEvVXNlcnMveGlhbWkvRG9jdW1lbnRzL3dvcmtzcGFjZS9hcG0vYXdzLW90ZWwtanMtaW5zdHJ1bWVudGF0aW9uL3NhbXBsZS1hcHBsaWNhdGlvbnMvc2ltcGxlLWV4cHJlc3Mtc2VydmVyL3NhbXBsZS1hcHAtZXhwcmVzcy1zZXJ2ZXIuanMKGAoNcHJvY2Vzcy5vd25lchIHCgV4aWFtaQoqCglob3N0Lm5hbWUSHQobODg2NjVhNGE5MGVhLmFudC5hbWF6b24uY29tChQKCWhvc3QuYXJjaBIHCgVhbWQ2NBAAErkFCi0KI0BvcGVudGVsZW1ldHJ5L2luc3RydW1lbnRhdGlvbi1odHRwEgYwLjUyLjEShwUKEGbP0iciZUFvR53IqY0B8dESCKTfctAxC8IAKg9HRVQgL2F3cy1zZGstczMwAjlAIw9lkw/wF0HmfZF8kw/wF0ouCghodHRwLnVybBIiCiBodHRwOi8vbG9jYWxob3N0OjgwODAvYXdzLXNkay1zM0odCglodHRwLmhvc3QSEAoObG9jYWxob3N0OjgwODBKHAoNbmV0Lmhvc3QubmFtZRILCglsb2NhbGhvc3RKFAoLaHR0cC5tZXRob2QSBQoDR0VUShUKC2h0dHAuc2NoZW1lEgYKBGh0dHBKHAoLaHR0cC50YXJnZXQSDQoLL2F3cy1zZGstczNKHwoPaHR0cC51c2VyX2FnZW50EgwKCmN1cmwvOC43LjFKFAoLaHR0cC5mbGF2b3ISBQoDMS4xShkKDW5ldC50cmFuc3BvcnQSCAoGaXBfdGNwShcKEWF3cy5pcy5sb2NhbC5yb290EgIQAUoUCgtuZXQuaG9zdC5pcBIFCgM6OjFKFAoNbmV0Lmhvc3QucG9ydBIDGJA/ShQKC25ldC5wZWVyLmlwEgUKAzo6MUoVCg1uZXQucGVlci5wb3J0EgQY4PgDShcKEGh0dHAuc3RhdHVzX2NvZGUSAxjIAUoYChBodHRwLnN0YXR1c190ZXh0EgQKAk9LShsKCmh0dHAucm91dGUSDQoLL2F3cy1zZGstczNKJQoRYXdzLmxvY2FsLnNlcnZpY2USEAoOTVlfQVBQTElDQVRJT05KKAoTYXdzLmxvY2FsLm9wZXJhdGlvbhIRCg9HRVQgL2F3cy1zZGstczNKHQoNYXdzLnNwYW4ua2luZBIMCgpMT0NBTF9ST09UUABgAHAAegIYAA== ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 612fefe commit da31402

File tree

5 files changed

+323
-4489
lines changed

5 files changed

+323
-4489
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-opentelemetry-configurator.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,14 @@ import { AwsSpanMetricsProcessorBuilder } from './aws-span-metrics-processor-bui
5858
import { AwsXRayRemoteSampler } from './sampler/aws-xray-remote-sampler';
5959
// This file is generated via `npm run compile`
6060
import { LIB_VERSION } from './version';
61+
import { OTLPUdpSpanExporter } from './otlp-udp-exporter';
6162

6263
const APPLICATION_SIGNALS_ENABLED_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_ENABLED';
6364
const APPLICATION_SIGNALS_EXPORTER_ENDPOINT_CONFIG: string = 'OTEL_AWS_APPLICATION_SIGNALS_EXPORTER_ENDPOINT';
6465
const METRIC_EXPORT_INTERVAL_CONFIG: string = 'OTEL_METRIC_EXPORT_INTERVAL';
6566
const DEFAULT_METRIC_EXPORT_INTERVAL_MILLIS: number = 60000;
67+
const AWS_LAMBDA_FUNCTION_NAME_CONFIG: string = 'AWS_LAMBDA_FUNCTION_NAME';
68+
const AWS_XRAY_DAEMON_ADDRESS_CONFIG: string = 'AWS_XRAY_DAEMON_ADDRESS';
6669

6770
/**
6871
* Aws Application Signals Config Provider creates a configuration object that can be provided to
@@ -395,15 +398,21 @@ export class AwsSpanProcessorProvider {
395398

396399
static configureOtlp(): SpanExporter {
397400
// eslint-disable-next-line @typescript-eslint/typedef
398-
const protocol = this.getOtlpProtocol();
401+
let protocol = this.getOtlpProtocol();
399402

403+
if (AwsOpentelemetryConfigurator.isApplicationSignalsEnabled() && isLambdaEnvironment()) {
404+
protocol = 'udp';
405+
}
400406
switch (protocol) {
401407
case 'grpc':
402408
return new OTLPGrpcTraceExporter();
403409
case 'http/json':
404410
return new OTLPHttpTraceExporter();
405411
case 'http/protobuf':
406412
return new OTLPProtoTraceExporter();
413+
case 'udp':
414+
diag.debug('Detected AWS Lambda environment and enabling UDPSpanExporter');
415+
return new OTLPUdpSpanExporter(process.env[AWS_XRAY_DAEMON_ADDRESS_CONFIG]);
407416
default:
408417
diag.warn(`Unsupported OTLP traces protocol: ${protocol}. Using http/protobuf.`);
409418
return new OTLPProtoTraceExporter();
@@ -591,4 +600,9 @@ function getSamplerProbabilityFromEnv(environment: Required<ENVIRONMENT>): numbe
591600

592601
return probability;
593602
}
603+
604+
export function isLambdaEnvironment() {
605+
// detect if running in AWS Lambda environment
606+
return process.env[AWS_LAMBDA_FUNCTION_NAME_CONFIG] !== undefined;
607+
}
594608
// END The OpenTelemetry Authors code
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import * as dgram from 'dgram';
5+
import { diag } from '@opentelemetry/api';
6+
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
7+
import { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer';
8+
import { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';
9+
10+
const DEFAULT_ENDPOINT = '127.0.0.1:2000';
11+
const PROTOCOL_HEADER = '{"format":"json","version":1}\n';
12+
const FORMAT_OTEL_TRACES_BINARY_PREFIX = 'T1';
13+
14+
export class UdpExporter {
15+
private _endpoint: string;
16+
private _host: string;
17+
private _port: number;
18+
private _socket: dgram.Socket;
19+
20+
constructor(endpoint?: string) {
21+
this._endpoint = endpoint || DEFAULT_ENDPOINT;
22+
[this._host, this._port] = this._parseEndpoint(this._endpoint);
23+
this._socket = dgram.createSocket('udp4');
24+
this._socket.unref();
25+
}
26+
27+
sendData(data: Uint8Array, signalFormatPrefix: string): void {
28+
const base64EncodedString = Buffer.from(data).toString('base64');
29+
const message = `${PROTOCOL_HEADER}${signalFormatPrefix}${base64EncodedString}`;
30+
31+
try {
32+
this._socket.send(Buffer.from(message, 'utf-8'), this._port, this._host, err => {
33+
if (err) {
34+
throw err;
35+
}
36+
});
37+
} catch (err) {
38+
diag.error('Error sending UDP data: %s', err);
39+
throw err;
40+
}
41+
}
42+
43+
shutdown(): void {
44+
this._socket.close();
45+
}
46+
47+
private _parseEndpoint(endpoint: string): [string, number] {
48+
try {
49+
const [host, port] = endpoint.split(':');
50+
return [host, parseInt(port, 10)];
51+
} catch (err) {
52+
throw new Error(`Invalid endpoint: ${endpoint}`);
53+
}
54+
}
55+
}
56+
57+
export class OTLPUdpSpanExporter implements SpanExporter {
58+
private _udpExporter: UdpExporter;
59+
private _signalPrefix: string;
60+
private _endpoint: string;
61+
62+
constructor(endpoint?: string, _signalPrefix?: string) {
63+
this._endpoint = endpoint || DEFAULT_ENDPOINT;
64+
this._udpExporter = new UdpExporter(this._endpoint);
65+
this._signalPrefix = _signalPrefix || FORMAT_OTEL_TRACES_BINARY_PREFIX;
66+
}
67+
68+
export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
69+
const serializedData = ProtobufTraceSerializer.serializeRequest(spans);
70+
if (serializedData == null) {
71+
return;
72+
}
73+
try {
74+
this._udpExporter.sendData(serializedData, this._signalPrefix);
75+
return resultCallback({ code: ExportResultCode.SUCCESS });
76+
} catch (err) {
77+
diag.error('Error exporting spans: %s', err);
78+
return resultCallback({ code: ExportResultCode.FAILED });
79+
}
80+
}
81+
82+
forceFlush(): Promise<void> {
83+
return Promise.resolve();
84+
}
85+
86+
/** Shutdown exporter. */
87+
shutdown(): Promise<void> {
88+
return new Promise((resolve, reject) => {
89+
try {
90+
this._udpExporter.shutdown();
91+
resolve();
92+
} catch (error) {
93+
reject(error);
94+
}
95+
});
96+
}
97+
}

aws-distro-opentelemetry-node-autoinstrumentation/test/aws-opentelemetry-configurator.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { setAwsDefaultEnvironmentVariables } from '../src/register';
3131
import { AwsXRayRemoteSampler } from '../src/sampler/aws-xray-remote-sampler';
3232
import { AwsXraySamplingClient } from '../src/sampler/aws-xray-sampling-client';
3333
import { GetSamplingRulesResponse } from '../src/sampler/remote-sampler.types';
34+
import { OTLPUdpSpanExporter } from '../src/otlp-udp-exporter';
3435

3536
// Tests AwsOpenTelemetryConfigurator after running Environment Variable setup in register.ts
3637
describe('AwsOpenTelemetryConfiguratorTest', () => {
@@ -327,6 +328,48 @@ describe('AwsOpenTelemetryConfiguratorTest', () => {
327328
delete process.env.OTEL_TRACES_SAMPLER_ARG;
328329
});
329330

331+
it('tests Span Exporter on Lambda with ApplicationSignals enabled', () => {
332+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'TestFunction';
333+
process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED = 'True';
334+
const mockExporter: SpanExporter = sinon.createStubInstance(OTLPUdpSpanExporter);
335+
const customizedExporter: SpanExporter = AwsSpanProcessorProvider.customizeSpanExporter(
336+
mockExporter,
337+
Resource.empty()
338+
);
339+
// should return UDP exporter for Lambda with AppSignals enabled
340+
expect((customizedExporter as any).delegate).toBeInstanceOf(OTLPUdpSpanExporter);
341+
delete process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED;
342+
delete process.env.AWS_LAMBDA_FUNCTION_NAME;
343+
});
344+
345+
it('tests Span Exporter on Lambda with ApplicationSignals disabled', () => {
346+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'TestFunction';
347+
process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED = 'False';
348+
const mockExporter: SpanExporter = sinon.createStubInstance(AwsMetricAttributesSpanExporter);
349+
const customizedExporter: SpanExporter = AwsSpanProcessorProvider.customizeSpanExporter(
350+
mockExporter,
351+
Resource.empty()
352+
);
353+
// should still return AwsMetricAttributesSpanExporter for Lambda if AppSignals disabled
354+
expect(mockExporter).toEqual(customizedExporter);
355+
delete process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED;
356+
delete process.env.AWS_LAMBDA_FUNCTION_NAME;
357+
});
358+
359+
it('tests configureOTLP on Lambda with ApplicationSignals enabled', () => {
360+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'TestFunction';
361+
process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED = 'True';
362+
process.env.OTEL_TRACES_EXPORTER = 'otlp';
363+
process.env.AWS_XRAY_DAEMON_ADDRESS = 'www.test.com:2222';
364+
const spanExporter: SpanExporter = AwsSpanProcessorProvider.configureOtlp();
365+
expect(spanExporter).toBeInstanceOf(OTLPUdpSpanExporter);
366+
expect((spanExporter as OTLPUdpSpanExporter)['_endpoint']).toBe('www.test.com:2222');
367+
delete process.env.OTEL_AWS_APPLICATION_SIGNALS_ENABLED;
368+
delete process.env.AWS_LAMBDA_FUNCTION_NAME;
369+
delete process.env.OTEL_TRACES_EXPORTER;
370+
delete process.env.AWS_XRAY_DAEMON_ADDRESS;
371+
});
372+
330373
function validateConfiguratorEnviron() {
331374
// Set by register.ts
332375
expect('http/protobuf').toEqual(process.env.OTEL_EXPORTER_OTLP_PROTOCOL);
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import { diag, SpanContext, SpanKind } from '@opentelemetry/api';
4+
import { ExportResultCode } from '@opentelemetry/core';
5+
import { Resource } from '@opentelemetry/resources';
6+
import { ProtobufTraceSerializer } from '@opentelemetry/otlp-transformer';
7+
import { OTLPUdpSpanExporter, UdpExporter } from '../src/otlp-udp-exporter';
8+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
9+
import * as sinon from 'sinon';
10+
import expect from 'expect';
11+
import { Socket } from 'dgram';
12+
13+
describe('UdpExporterTest', () => {
14+
const endpoint = '127.0.0.1:3000';
15+
const host = '127.0.0.1';
16+
const port = 3000;
17+
let udpExporter: UdpExporter;
18+
let socketSend: sinon.SinonStub<any[], any>;
19+
let socketClose: sinon.SinonStub<[callback?: (() => void) | undefined], Socket>;
20+
let diagErrorSpy: sinon.SinonSpy<[message: string, ...args: unknown[]], void>;
21+
22+
beforeEach(() => {
23+
udpExporter = new UdpExporter(endpoint);
24+
25+
// Stub the _socket methods
26+
socketSend = sinon.stub(udpExporter['_socket'], 'send');
27+
socketClose = sinon.stub(udpExporter['_socket'], 'close');
28+
29+
// Spy on diag.error using sinon
30+
diagErrorSpy = sinon.spy(diag, 'error');
31+
});
32+
33+
afterEach(() => {
34+
sinon.restore(); // Restore the original dgram behavior
35+
});
36+
37+
it('should parse the endpoint correctly', () => {
38+
expect(udpExporter['_host']).toBe(host);
39+
expect(udpExporter['_port']).toBe(port);
40+
});
41+
42+
it('should send UDP data correctly', () => {
43+
const data = new Uint8Array([1, 2, 3]);
44+
const prefix = 'T1';
45+
const encodedData = '{"format":"json","version":1}\nT1AQID';
46+
const protbufBinary = Buffer.from(encodedData, 'utf-8');
47+
udpExporter.sendData(data, prefix);
48+
sinon.assert.calledOnce(socketSend);
49+
expect(socketSend.getCall(0).args[0]).toEqual(protbufBinary);
50+
});
51+
52+
it('should handle errors when sending UDP data', () => {
53+
const errorMessage = 'UDP send error';
54+
socketSend.yields(new Error(errorMessage)); // Simulate an error
55+
56+
const data = new Uint8Array([1, 2, 3]);
57+
// Expect the sendData method to throw the error
58+
expect(() => udpExporter.sendData(data, 'T1')).toThrow(errorMessage);
59+
// Assert that diag.error was called with the correct error message
60+
expect(diagErrorSpy.calledOnce).toBe(true);
61+
expect(diagErrorSpy.calledWith('Error sending UDP data: %s', sinon.match.instanceOf(Error))).toBe(true);
62+
});
63+
64+
it('should close the socket on shutdown', () => {
65+
udpExporter.shutdown();
66+
expect(socketClose.calledOnce).toBe(true);
67+
});
68+
});
69+
70+
describe('OTLPUdpSpanExporterTest', () => {
71+
let otlpUdpSpanExporter: OTLPUdpSpanExporter;
72+
let udpExporterMock: { sendData: any; shutdown: any };
73+
let diagErrorSpy: sinon.SinonSpy<[message: string, ...args: unknown[]], void>;
74+
const endpoint = '127.0.0.1:3000';
75+
const prefix = 'T1';
76+
const serializedData = new Uint8Array([1, 2, 3]); // Mock serialized data
77+
// Mock ReadableSpan object
78+
const mockSpanData: ReadableSpan = {
79+
name: 'spanName',
80+
kind: SpanKind.SERVER,
81+
spanContext: () => {
82+
const spanContext: SpanContext = {
83+
traceId: '00000000000000000000000000000008',
84+
spanId: '0000000000000009',
85+
traceFlags: 0,
86+
};
87+
return spanContext;
88+
},
89+
startTime: [0, 0],
90+
endTime: [0, 1],
91+
status: { code: 0 },
92+
attributes: {},
93+
links: [],
94+
events: [],
95+
duration: [0, 1],
96+
ended: true,
97+
resource: new Resource({}),
98+
instrumentationLibrary: { name: 'mockedLibrary' },
99+
droppedAttributesCount: 0,
100+
droppedEventsCount: 0,
101+
droppedLinksCount: 0,
102+
};
103+
const spans: ReadableSpan[] = [mockSpanData]; // Mock span data
104+
105+
beforeEach(() => {
106+
// Mock UdpExporter methods
107+
udpExporterMock = {
108+
sendData: sinon.stub(),
109+
shutdown: sinon.stub().resolves(),
110+
};
111+
112+
// Stub the UdpExporter constructor to return our mock
113+
sinon.stub(UdpExporter.prototype, 'sendData').callsFake(udpExporterMock.sendData);
114+
sinon.stub(UdpExporter.prototype, 'shutdown').callsFake(udpExporterMock.shutdown);
115+
116+
// Stub the diag.error method
117+
diagErrorSpy = sinon.spy(diag, 'error');
118+
119+
// Create an instance of OTLPUdpSpanExporter
120+
otlpUdpSpanExporter = new OTLPUdpSpanExporter(endpoint, prefix);
121+
});
122+
123+
afterEach(() => {
124+
// Restore the original functionality after each test
125+
sinon.restore();
126+
});
127+
128+
it('should export spans successfully', () => {
129+
const callback = sinon.stub();
130+
// Stub ProtobufTraceSerializer.serializeRequest
131+
sinon.stub(ProtobufTraceSerializer, 'serializeRequest').returns(serializedData);
132+
133+
otlpUdpSpanExporter.export(spans, callback);
134+
135+
expect(udpExporterMock.sendData.calledOnceWith(serializedData, 'T1')).toBe(true);
136+
expect(callback.calledOnceWith({ code: ExportResultCode.SUCCESS })).toBe(true);
137+
expect(diagErrorSpy.notCalled).toBe(true); // Ensure no error was logged
138+
});
139+
140+
it('should handle serialization failure', () => {
141+
// Make serializeRequest return null
142+
sinon.stub(ProtobufTraceSerializer, 'serializeRequest').returns(undefined);
143+
const callback = sinon.stub();
144+
145+
otlpUdpSpanExporter.export(spans, callback);
146+
147+
expect(callback.notCalled).toBe(true);
148+
expect(udpExporterMock.sendData.notCalled).toBe(true);
149+
expect(diagErrorSpy.notCalled).toBe(true);
150+
});
151+
152+
it('should handle errors during export', () => {
153+
const error = new Error('Export error');
154+
udpExporterMock.sendData.throws(error);
155+
156+
const callback = sinon.stub();
157+
158+
otlpUdpSpanExporter.export(spans, callback);
159+
160+
expect(diagErrorSpy.calledOnceWith('Error exporting spans: %s', sinon.match.instanceOf(Error))).toBe(true);
161+
expect(callback.calledOnceWith({ code: ExportResultCode.FAILED })).toBe(true);
162+
});
163+
164+
it('should shutdown the UDP exporter successfully', async () => {
165+
await otlpUdpSpanExporter.shutdown();
166+
expect(udpExporterMock.shutdown.calledOnce).toBe(true);
167+
});
168+
});

0 commit comments

Comments
 (0)