Skip to content

Commit 76c4667

Browse files
committed
test(otlp-transformer): add tests for trace serializer
1 parent 79256ed commit 76c4667

File tree

2 files changed

+267
-64
lines changed

2 files changed

+267
-64
lines changed

experimental/packages/otlp-transformer/src/json/serializers.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ export const JsonTraceSerializer: ISerializer<
3636
const encoder = new TextEncoder();
3737
return encoder.encode(JSON.stringify(request));
3838
},
39-
deserializeResponse: (_arg: Uint8Array) => {
40-
// Not yet implemented
41-
return {};
39+
deserializeResponse: (arg: Uint8Array) => {
40+
const decoder = new TextDecoder();
41+
return JSON.parse(decoder.decode(arg)) as IExportTraceServiceResponse;
4242
},
4343
};
4444

@@ -54,9 +54,9 @@ export const JsonMetricsSerializer: ISerializer<
5454
const encoder = new TextEncoder();
5555
return encoder.encode(JSON.stringify(request));
5656
},
57-
deserializeResponse: (_arg: Uint8Array) => {
58-
// Not yet implemented
59-
return {};
57+
deserializeResponse: (arg: Uint8Array) => {
58+
const decoder = new TextDecoder();
59+
return JSON.parse(decoder.decode(arg)) as IExportMetricsServiceResponse;
6060
},
6161
};
6262

@@ -72,8 +72,8 @@ export const JsonLogsSerializer: ISerializer<
7272
const encoder = new TextEncoder();
7373
return encoder.encode(JSON.stringify(request));
7474
},
75-
deserializeResponse: (_arg: Uint8Array) => {
76-
// Not yet implemented
77-
return {};
75+
deserializeResponse: (arg: Uint8Array) => {
76+
const decoder = new TextDecoder();
77+
return JSON.parse(decoder.decode(arg)) as IExportLogsServiceResponse;
7878
},
7979
};

experimental/packages/otlp-transformer/test/trace.test.ts

Lines changed: 258 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
import * as root from '../src/generated/root';
1617
import { SpanKind, SpanStatusCode, TraceFlags } from '@opentelemetry/api';
1718
import { TraceState, hexToBinary } from '@opentelemetry/core';
1819
import { Resource } from '@opentelemetry/resources';
@@ -23,6 +24,8 @@ import {
2324
createExportTraceServiceRequest,
2425
ESpanKind,
2526
EStatusCode,
27+
ProtobufTraceSerializer,
28+
JsonTraceSerializer,
2629
} from '../src';
2730

2831
function createExpectedSpanJson(options: OtlpEncodingOptions) {
@@ -133,69 +136,173 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) {
133136
};
134137
}
135138

136-
describe('Trace', () => {
137-
describe('createExportTraceServiceRequest', () => {
138-
let resource: Resource;
139-
let span: ReadableSpan;
139+
/**
140+
* utility function to convert a string representing a hex value to a base64 string
141+
* that represents the bytes of that hex value. This is needed as we need to support Node.js 14
142+
* where btoa() does not exist, and the Browser, where Buffer does not exist.
143+
* @param hexStr
144+
*/
145+
function toBase64(hexStr: string) {
146+
if (typeof btoa !== 'undefined') {
147+
const decoder = new TextDecoder('utf8');
148+
return btoa(decoder.decode(hexToBinary(hexStr)));
149+
}
140150

141-
beforeEach(() => {
142-
resource = new Resource({
143-
'resource-attribute': 'resource attribute value',
144-
});
145-
span = {
146-
spanContext: () => ({
147-
spanId: '0000000000000002',
148-
traceFlags: TraceFlags.SAMPLED,
149-
traceId: '00000000000000000000000000000001',
150-
isRemote: false,
151-
traceState: new TraceState('span=bar'),
152-
}),
153-
parentSpanId: '0000000000000001',
154-
attributes: { 'string-attribute': 'some attribute value' },
155-
duration: [1, 300000000],
156-
endTime: [1640715558, 642725388],
157-
ended: true,
158-
events: [
159-
{
160-
name: 'some event',
161-
time: [1640715558, 542725388],
162-
attributes: {
163-
'event-attribute': 'some string value',
151+
return Buffer.from(hexToBinary(hexStr)).toString('base64');
152+
}
153+
154+
function createExpectedSpanProtobuf() {
155+
const startTime = 1640715557342725400;
156+
const endTime = 1640715558642725400;
157+
const eventTime = 1640715558542725400;
158+
159+
const traceId = toBase64('00000000000000000000000000000001');
160+
const spanId = toBase64('0000000000000002');
161+
const parentSpanId = toBase64('0000000000000001');
162+
const linkSpanId = toBase64('0000000000000003');
163+
const linkTraceId = toBase64('00000000000000000000000000000002');
164+
165+
return {
166+
resourceSpans: [
167+
{
168+
resource: {
169+
attributes: [
170+
{
171+
key: 'resource-attribute',
172+
value: { stringValue: 'resource attribute value' },
164173
},
165-
},
166-
],
167-
instrumentationLibrary: {
168-
name: 'myLib',
169-
version: '0.1.0',
170-
schemaUrl: 'http://url.to.schema',
174+
],
175+
droppedAttributesCount: 0,
171176
},
172-
kind: SpanKind.CLIENT,
173-
links: [
177+
scopeSpans: [
174178
{
175-
context: {
176-
spanId: '0000000000000003',
177-
traceId: '00000000000000000000000000000002',
178-
traceFlags: TraceFlags.SAMPLED,
179-
isRemote: false,
180-
traceState: new TraceState('link=foo'),
181-
},
182-
attributes: {
183-
'link-attribute': 'string value',
184-
},
179+
scope: { name: 'myLib', version: '0.1.0' },
180+
spans: [
181+
{
182+
traceId: traceId,
183+
spanId: spanId,
184+
traceState: 'span=bar',
185+
parentSpanId: parentSpanId,
186+
name: 'span-name',
187+
kind: ESpanKind.SPAN_KIND_CLIENT,
188+
links: [
189+
{
190+
droppedAttributesCount: 0,
191+
spanId: linkSpanId,
192+
traceId: linkTraceId,
193+
traceState: 'link=foo',
194+
attributes: [
195+
{
196+
key: 'link-attribute',
197+
value: {
198+
stringValue: 'string value',
199+
},
200+
},
201+
],
202+
},
203+
],
204+
startTimeUnixNano: startTime,
205+
endTimeUnixNano: endTime,
206+
events: [
207+
{
208+
droppedAttributesCount: 0,
209+
attributes: [
210+
{
211+
key: 'event-attribute',
212+
value: {
213+
stringValue: 'some string value',
214+
},
215+
},
216+
],
217+
name: 'some event',
218+
timeUnixNano: eventTime,
219+
},
220+
],
221+
attributes: [
222+
{
223+
key: 'string-attribute',
224+
value: { stringValue: 'some attribute value' },
225+
},
226+
],
227+
droppedAttributesCount: 0,
228+
droppedEventsCount: 0,
229+
droppedLinksCount: 0,
230+
status: {
231+
code: EStatusCode.STATUS_CODE_OK,
232+
},
233+
},
234+
],
235+
schemaUrl: 'http://url.to.schema',
185236
},
186237
],
187-
name: 'span-name',
188-
resource,
189-
startTime: [1640715557, 342725388],
190-
status: {
191-
code: SpanStatusCode.OK,
192-
},
193-
droppedAttributesCount: 0,
194-
droppedEventsCount: 0,
195-
droppedLinksCount: 0,
196-
};
238+
},
239+
],
240+
};
241+
}
242+
243+
describe('Trace', () => {
244+
let resource: Resource;
245+
let span: ReadableSpan;
246+
247+
beforeEach(() => {
248+
resource = new Resource({
249+
'resource-attribute': 'resource attribute value',
197250
});
251+
span = {
252+
spanContext: () => ({
253+
spanId: '0000000000000002',
254+
traceFlags: TraceFlags.SAMPLED,
255+
traceId: '00000000000000000000000000000001',
256+
isRemote: false,
257+
traceState: new TraceState('span=bar'),
258+
}),
259+
parentSpanId: '0000000000000001',
260+
attributes: { 'string-attribute': 'some attribute value' },
261+
duration: [1, 300000000],
262+
endTime: [1640715558, 642725388],
263+
ended: true,
264+
events: [
265+
{
266+
name: 'some event',
267+
time: [1640715558, 542725388],
268+
attributes: {
269+
'event-attribute': 'some string value',
270+
},
271+
},
272+
],
273+
instrumentationLibrary: {
274+
name: 'myLib',
275+
version: '0.1.0',
276+
schemaUrl: 'http://url.to.schema',
277+
},
278+
kind: SpanKind.CLIENT,
279+
links: [
280+
{
281+
context: {
282+
spanId: '0000000000000003',
283+
traceId: '00000000000000000000000000000002',
284+
traceFlags: TraceFlags.SAMPLED,
285+
isRemote: false,
286+
traceState: new TraceState('link=foo'),
287+
},
288+
attributes: {
289+
'link-attribute': 'string value',
290+
},
291+
},
292+
],
293+
name: 'span-name',
294+
resource,
295+
startTime: [1640715557, 342725388],
296+
status: {
297+
code: SpanStatusCode.OK,
298+
},
299+
droppedAttributesCount: 0,
300+
droppedEventsCount: 0,
301+
droppedLinksCount: 0,
302+
};
303+
});
198304

305+
describe('createExportTraceServiceRequest', () => {
199306
it('returns null on an empty list', () => {
200307
assert.deepStrictEqual(
201308
createExportTraceServiceRequest([], { useHex: true }),
@@ -343,4 +450,100 @@ describe('Trace', () => {
343450
});
344451
});
345452
});
453+
454+
describe('ProtobufTracesSerializer', function () {
455+
it('serializes an export request', () => {
456+
const serialized = ProtobufTraceSerializer.serializeRequest([span]);
457+
assert.ok(serialized, 'serialized response is undefined');
458+
const decoded =
459+
root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest.decode(
460+
serialized
461+
);
462+
463+
const expected = createExpectedSpanProtobuf();
464+
const decodedObj =
465+
root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest.toObject(
466+
decoded,
467+
{
468+
// This incurs some precision loss that's taken into account in createExpectedSpanProtobuf()
469+
// Using String here will incur the same precision loss on browser only, using Number to prevent having to
470+
// have different assertions for browser and Node.js
471+
longs: Number,
472+
// Convert to String (Base64) as otherwise the type will be different for Node.js (Buffer) and Browser (Uint8Array)
473+
// and this fails assertions.
474+
bytes: String,
475+
}
476+
);
477+
478+
assert.deepStrictEqual(decodedObj, expected);
479+
});
480+
481+
it('deserializes a response', () => {
482+
const protobufSerializedResponse =
483+
root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse.encode(
484+
{
485+
partialSuccess: {
486+
errorMessage: 'foo',
487+
rejectedSpans: 1,
488+
},
489+
}
490+
).finish();
491+
492+
const deserializedResponse = ProtobufTraceSerializer.deserializeResponse(
493+
protobufSerializedResponse
494+
);
495+
496+
assert.ok(
497+
deserializedResponse.partialSuccess,
498+
'partialSuccess not present in the deserialized message'
499+
);
500+
assert.equal(deserializedResponse.partialSuccess.errorMessage, 'foo');
501+
assert.equal(
502+
Number(deserializedResponse.partialSuccess.rejectedSpans),
503+
1
504+
);
505+
});
506+
});
507+
508+
describe('JsonTracesSerializer', function () {
509+
it('serializes an export request', () => {
510+
// stringify, then parse to remove undefined keys in the expected JSON
511+
const expected = JSON.parse(
512+
JSON.stringify(
513+
createExpectedSpanJson({
514+
useHex: true,
515+
useLongBits: false,
516+
})
517+
)
518+
);
519+
const serialized = JsonTraceSerializer.serializeRequest([span]);
520+
521+
const decoder = new TextDecoder();
522+
assert.deepStrictEqual(JSON.parse(decoder.decode(serialized)), expected);
523+
});
524+
525+
it('deserializes a response', () => {
526+
const expectedResponse = {
527+
partialSuccess: {
528+
errorMessage: 'foo',
529+
rejectedSpans: 1,
530+
},
531+
};
532+
const encoder = new TextEncoder();
533+
const encodedResponse = encoder.encode(JSON.stringify(expectedResponse));
534+
535+
const deserializedResponse =
536+
JsonTraceSerializer.deserializeResponse(encodedResponse);
537+
538+
assert.ok(
539+
deserializedResponse.partialSuccess,
540+
'partialSuccess not present in the deserialized message'
541+
);
542+
assert.equal(deserializedResponse.partialSuccess.errorMessage, 'foo');
543+
assert.equal(
544+
Number(deserializedResponse.partialSuccess.rejectedSpans),
545+
1
546+
);
547+
});
548+
});
346549
});

0 commit comments

Comments
 (0)