From e3ce9e4bc4bba8a4447867cdf069a9c77fd52f80 Mon Sep 17 00:00:00 2001 From: Marc Pichler Date: Fri, 15 Mar 2024 17:00:40 +0100 Subject: [PATCH] test(otlp-transformer): add tests for trace serializer --- .../otlp-transformer/src/json/serializers.ts | 18 +- .../otlp-transformer/test/trace.test.ts | 304 ++++++++++++++---- 2 files changed, 258 insertions(+), 64 deletions(-) diff --git a/experimental/packages/otlp-transformer/src/json/serializers.ts b/experimental/packages/otlp-transformer/src/json/serializers.ts index fe78aeedfc6..45c42fc81da 100644 --- a/experimental/packages/otlp-transformer/src/json/serializers.ts +++ b/experimental/packages/otlp-transformer/src/json/serializers.ts @@ -36,9 +36,9 @@ export const JsonTraceSerializer: ISerializer< const encoder = new TextEncoder(); return encoder.encode(JSON.stringify(request)); }, - deserializeResponse: (_arg: Uint8Array) => { - // Not yet implemented - return {}; + deserializeResponse: (arg: Uint8Array) => { + const decoder = new TextDecoder(); + return JSON.parse(decoder.decode(arg)) as IExportTraceServiceResponse; }, }; @@ -54,9 +54,9 @@ export const JsonMetricsSerializer: ISerializer< const encoder = new TextEncoder(); return encoder.encode(JSON.stringify(request)); }, - deserializeResponse: (_arg: Uint8Array) => { - // Not yet implemented - return {}; + deserializeResponse: (arg: Uint8Array) => { + const decoder = new TextDecoder(); + return JSON.parse(decoder.decode(arg)) as IExportMetricsServiceResponse; }, }; @@ -72,8 +72,8 @@ export const JsonLogsSerializer: ISerializer< const encoder = new TextEncoder(); return encoder.encode(JSON.stringify(request)); }, - deserializeResponse: (_arg: Uint8Array) => { - // Not yet implemented - return {}; + deserializeResponse: (arg: Uint8Array) => { + const decoder = new TextDecoder(); + return JSON.parse(decoder.decode(arg)) as IExportLogsServiceResponse; }, }; diff --git a/experimental/packages/otlp-transformer/test/trace.test.ts b/experimental/packages/otlp-transformer/test/trace.test.ts index 65b23ddc2dd..7fe4e0eb3a8 100644 --- a/experimental/packages/otlp-transformer/test/trace.test.ts +++ b/experimental/packages/otlp-transformer/test/trace.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import * as root from '../src/generated/root'; import { SpanKind, SpanStatusCode, TraceFlags } from '@opentelemetry/api'; import { TraceState, hexToBinary } from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; @@ -23,6 +24,8 @@ import { createExportTraceServiceRequest, ESpanKind, EStatusCode, + ProtobufTraceSerializer, + JsonTraceSerializer, } from '../src'; function createExpectedSpanJson(options: OtlpEncodingOptions) { @@ -133,69 +136,164 @@ function createExpectedSpanJson(options: OtlpEncodingOptions) { }; } -describe('Trace', () => { - describe('createExportTraceServiceRequest', () => { - let resource: Resource; - let span: ReadableSpan; +function createExpectedSpanProtobuf() { + const startTime = 1640715557342725400; + const endTime = 1640715558642725400; + const eventTime = 1640715558542725400; - beforeEach(() => { - resource = new Resource({ - 'resource-attribute': 'resource attribute value', - }); - span = { - spanContext: () => ({ - spanId: '0000000000000002', - traceFlags: TraceFlags.SAMPLED, - traceId: '00000000000000000000000000000001', - isRemote: false, - traceState: new TraceState('span=bar'), - }), - parentSpanId: '0000000000000001', - attributes: { 'string-attribute': 'some attribute value' }, - duration: [1, 300000000], - endTime: [1640715558, 642725388], - ended: true, - events: [ - { - name: 'some event', - time: [1640715558, 542725388], - attributes: { - 'event-attribute': 'some string value', + const decoder = new TextDecoder('utf8'); + + const traceId = btoa( + decoder.decode(hexToBinary('00000000000000000000000000000001')) + ); + const spanId = btoa(decoder.decode(hexToBinary('0000000000000002'))); + const parentSpanId = btoa(decoder.decode(hexToBinary('0000000000000001'))); + const linkSpanId = btoa(decoder.decode(hexToBinary('0000000000000003'))); + const linkTraceId = btoa( + decoder.decode(hexToBinary('00000000000000000000000000000002')) + ); + + return { + resourceSpans: [ + { + resource: { + attributes: [ + { + key: 'resource-attribute', + value: { stringValue: 'resource attribute value' }, }, - }, - ], - instrumentationLibrary: { - name: 'myLib', - version: '0.1.0', - schemaUrl: 'http://url.to.schema', + ], + droppedAttributesCount: 0, }, - kind: SpanKind.CLIENT, - links: [ + scopeSpans: [ { - context: { - spanId: '0000000000000003', - traceId: '00000000000000000000000000000002', - traceFlags: TraceFlags.SAMPLED, - isRemote: false, - traceState: new TraceState('link=foo'), - }, - attributes: { - 'link-attribute': 'string value', - }, + scope: { name: 'myLib', version: '0.1.0' }, + spans: [ + { + traceId: traceId, + spanId: spanId, + traceState: 'span=bar', + parentSpanId: parentSpanId, + name: 'span-name', + kind: ESpanKind.SPAN_KIND_CLIENT, + links: [ + { + droppedAttributesCount: 0, + spanId: linkSpanId, + traceId: linkTraceId, + traceState: 'link=foo', + attributes: [ + { + key: 'link-attribute', + value: { + stringValue: 'string value', + }, + }, + ], + }, + ], + startTimeUnixNano: startTime, + endTimeUnixNano: endTime, + events: [ + { + droppedAttributesCount: 0, + attributes: [ + { + key: 'event-attribute', + value: { + stringValue: 'some string value', + }, + }, + ], + name: 'some event', + timeUnixNano: eventTime, + }, + ], + attributes: [ + { + key: 'string-attribute', + value: { stringValue: 'some attribute value' }, + }, + ], + droppedAttributesCount: 0, + droppedEventsCount: 0, + droppedLinksCount: 0, + status: { + code: EStatusCode.STATUS_CODE_OK, + }, + }, + ], + schemaUrl: 'http://url.to.schema', }, ], - name: 'span-name', - resource, - startTime: [1640715557, 342725388], - status: { - code: SpanStatusCode.OK, - }, - droppedAttributesCount: 0, - droppedEventsCount: 0, - droppedLinksCount: 0, - }; + }, + ], + }; +} + +describe('Trace', () => { + let resource: Resource; + let span: ReadableSpan; + + beforeEach(() => { + resource = new Resource({ + 'resource-attribute': 'resource attribute value', }); + span = { + spanContext: () => ({ + spanId: '0000000000000002', + traceFlags: TraceFlags.SAMPLED, + traceId: '00000000000000000000000000000001', + isRemote: false, + traceState: new TraceState('span=bar'), + }), + parentSpanId: '0000000000000001', + attributes: { 'string-attribute': 'some attribute value' }, + duration: [1, 300000000], + endTime: [1640715558, 642725388], + ended: true, + events: [ + { + name: 'some event', + time: [1640715558, 542725388], + attributes: { + 'event-attribute': 'some string value', + }, + }, + ], + instrumentationLibrary: { + name: 'myLib', + version: '0.1.0', + schemaUrl: 'http://url.to.schema', + }, + kind: SpanKind.CLIENT, + links: [ + { + context: { + spanId: '0000000000000003', + traceId: '00000000000000000000000000000002', + traceFlags: TraceFlags.SAMPLED, + isRemote: false, + traceState: new TraceState('link=foo'), + }, + attributes: { + 'link-attribute': 'string value', + }, + }, + ], + name: 'span-name', + resource, + startTime: [1640715557, 342725388], + status: { + code: SpanStatusCode.OK, + }, + droppedAttributesCount: 0, + droppedEventsCount: 0, + droppedLinksCount: 0, + }; + }); + describe('createExportTraceServiceRequest', () => { it('returns null on an empty list', () => { assert.deepStrictEqual( createExportTraceServiceRequest([], { useHex: true }), @@ -343,4 +441,100 @@ describe('Trace', () => { }); }); }); + + describe('ProtobufTracesSerializer', function () { + it('serializes an export request', () => { + const serialized = ProtobufTraceSerializer.serializeRequest([span]); + assert.ok(serialized, 'serialized response is undefined'); + const decoded = + root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest.decode( + serialized + ); + + const expected = createExpectedSpanProtobuf(); + const decodedObj = + root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest.toObject( + decoded, + { + // This incurs some precision loss that's taken into account in createExpectedSpanProtobuf() + // Using String here will incur the same precision loss on browser only, using Number to prevent having to + // have different assertions for browser and Node.js + longs: Number, + // Convert to String (Base64) as otherwise the type will be different for Node.js (Buffer) and Browser (Uint8Array) + // and this fails assertions. + bytes: String, + } + ); + + assert.deepStrictEqual(decodedObj, expected); + }); + + it('deserializes a response', () => { + const protobufSerializedResponse = + root.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse.encode( + { + partialSuccess: { + errorMessage: 'foo', + rejectedSpans: 1, + }, + } + ).finish(); + + const deserializedResponse = ProtobufTraceSerializer.deserializeResponse( + protobufSerializedResponse + ); + + assert.ok( + deserializedResponse.partialSuccess, + 'partialSuccess not present in the deserialized message' + ); + assert.equal(deserializedResponse.partialSuccess.errorMessage, 'foo'); + assert.equal( + Number(deserializedResponse.partialSuccess.rejectedSpans), + 1 + ); + }); + }); + + describe('JsonTracesSerializer', function () { + it('serializes an export request', () => { + // stringify, then parse to remove undefined keys in the expected JSON + const expected = JSON.parse( + JSON.stringify( + createExpectedSpanJson({ + useHex: true, + useLongBits: false, + }) + ) + ); + const serialized = JsonTraceSerializer.serializeRequest([span]); + + const decoder = new TextDecoder(); + assert.deepStrictEqual(JSON.parse(decoder.decode(serialized)), expected); + }); + + it('deserializes a response', () => { + const expectedResponse = { + partialSuccess: { + errorMessage: 'foo', + rejectedSpans: 1, + }, + }; + const encoder = new TextEncoder(); + const encodedResponse = encoder.encode(JSON.stringify(expectedResponse)); + + const deserializedResponse = + JsonTraceSerializer.deserializeResponse(encodedResponse); + + assert.ok( + deserializedResponse.partialSuccess, + 'partialSuccess not present in the deserialized message' + ); + assert.equal(deserializedResponse.partialSuccess.errorMessage, 'foo'); + assert.equal( + Number(deserializedResponse.partialSuccess.rejectedSpans), + 1 + ); + }); + }); });