diff --git a/propagators/propagator-aws-xray/src/AWSXRayPropagator.ts b/propagators/propagator-aws-xray/src/AWSXRayPropagator.ts index 06f535ad93..723f84341f 100644 --- a/propagators/propagator-aws-xray/src/AWSXRayPropagator.ts +++ b/propagators/propagator-aws-xray/src/AWSXRayPropagator.ts @@ -27,7 +27,8 @@ import { isValidTraceId, INVALID_TRACEID, INVALID_SPANID, - INVALID_SPAN_CONTEXT, + propagation, + Baggage, } from '@opentelemetry/api'; export const AWSXRAY_TRACE_ID_HEADER = 'x-amzn-trace-id'; @@ -49,6 +50,12 @@ const SAMPLED_FLAG_KEY = 'Sampled'; const IS_SAMPLED = '1'; const NOT_SAMPLED = '0'; +const LINEAGE_KEY = 'Lineage'; +const LINEAGE_DELIMITER = ':'; +const LINEAGE_HASH_LENGTH = 8; +const LINEAGE_MAX_COUNTER_1 = 255; +const LINEAGE_MAX_COUNTER_2 = 32767; + /** * Implementation of the AWS X-Ray Trace Header propagation protocol. See AWS @@ -66,6 +73,8 @@ export class AWSXRayPropagator implements TextMapPropagator { const timestamp = otTraceId.substring(0, TRACE_ID_FIRST_PART_LENGTH); const randomNumber = otTraceId.substring(TRACE_ID_FIRST_PART_LENGTH); + const xrayTraceId = `${TRACE_ID_VERSION}${TRACE_ID_DELIMITER}${timestamp}${TRACE_ID_DELIMITER}${randomNumber}`; + const parentId = spanContext.spanId; const samplingFlag = (TraceFlags.SAMPLED & spanContext.traceFlags) === TraceFlags.SAMPLED @@ -73,31 +82,52 @@ export class AWSXRayPropagator implements TextMapPropagator { : NOT_SAMPLED; // TODO: Add OT trace state to the X-Ray trace header - const traceHeader = `Root=1-${timestamp}-${randomNumber};Parent=${parentId};Sampled=${samplingFlag}`; + let traceHeader = + `${TRACE_ID_KEY}` + + `${KV_DELIMITER}` + + `${xrayTraceId}` + + `${TRACE_HEADER_DELIMITER}` + + `${PARENT_ID_KEY}` + + `${KV_DELIMITER}` + + `${parentId}` + + `${TRACE_HEADER_DELIMITER}` + + `${SAMPLED_FLAG_KEY}` + + `${KV_DELIMITER}` + + `${samplingFlag}`; + + const baggage = propagation.getBaggage(context); + const lineageV2Header = baggage?.getEntry(LINEAGE_KEY)?.value; + + if (lineageV2Header) { + traceHeader += + `${TRACE_HEADER_DELIMITER}` + + `${LINEAGE_KEY}` + + `${KV_DELIMITER}` + + `${lineageV2Header}`; + } + setter.set(carrier, AWSXRAY_TRACE_ID_HEADER, traceHeader); } extract(context: Context, carrier: unknown, getter: TextMapGetter): Context { - const spanContext = this.getSpanContextFromHeader(carrier, getter); - if (!isSpanContextValid(spanContext)) return context; - - return trace.setSpan(context, trace.wrapSpanContext(spanContext)); + return this.getContextFromHeader(context, carrier, getter); } fields(): string[] { return [AWSXRAY_TRACE_ID_HEADER]; } - private getSpanContextFromHeader( + private getContextFromHeader( + context: Context, carrier: unknown, getter: TextMapGetter - ): SpanContext { + ): Context { const headerKeys = getter.keys(carrier); const relevantHeaderKey = headerKeys.find(e => { return e.toLowerCase() === AWSXRAY_TRACE_ID_HEADER; }); if (!relevantHeaderKey) { - return INVALID_SPAN_CONTEXT; + return context; } const rawTraceHeader = getter.get(carrier, relevantHeaderKey); const traceHeader = Array.isArray(rawTraceHeader) @@ -105,9 +135,12 @@ export class AWSXRayPropagator implements TextMapPropagator { : rawTraceHeader; if (!traceHeader || typeof traceHeader !== 'string') { - return INVALID_SPAN_CONTEXT; + return context; } + let baggage: Baggage = + propagation.getBaggage(context) || propagation.createBaggage(); + let pos = 0; let trimmedPart: string; let parsedTraceId = INVALID_TRACEID; @@ -133,10 +166,14 @@ export class AWSXRayPropagator implements TextMapPropagator { parsedSpanId = AWSXRayPropagator._parseSpanId(value); } else if (trimmedPart.startsWith(SAMPLED_FLAG_KEY)) { parsedTraceFlags = AWSXRayPropagator._parseTraceFlag(value); + } else if (trimmedPart.startsWith(LINEAGE_KEY)) { + if (AWSXRayPropagator._isValidLineageV2Header(value)) { + baggage = baggage.setEntry(LINEAGE_KEY, { value }); + } } } if (parsedTraceFlags === null) { - return INVALID_SPAN_CONTEXT; + return context; } const resultSpanContext: SpanContext = { traceId: parsedTraceId, @@ -144,10 +181,17 @@ export class AWSXRayPropagator implements TextMapPropagator { traceFlags: parsedTraceFlags, isRemote: true, }; - if (!isSpanContextValid(resultSpanContext)) { - return INVALID_SPAN_CONTEXT; + if (isSpanContextValid(resultSpanContext)) { + context = trace.setSpan( + context, + trace.wrapSpanContext(resultSpanContext) + ); } - return resultSpanContext; + if (baggage.getAllEntries().length > 0) { + context = propagation.setBaggage(context, baggage); + } + + return context; } private static _parseTraceId(xrayTraceId: string): string { @@ -191,6 +235,27 @@ export class AWSXRayPropagator implements TextMapPropagator { return isValidSpanId(xrayParentId) ? xrayParentId : INVALID_SPANID; } + private static _isValidLineageV2Header(xrayLineageHeader: string): boolean { + const lineageSubstrings = xrayLineageHeader.split(LINEAGE_DELIMITER); + if (lineageSubstrings.length !== 3) { + return false; + } + + const lineageCounter1 = parseInt(lineageSubstrings[0]); + const hashedString = lineageSubstrings[1]; + const lineageCounter2 = parseInt(lineageSubstrings[2]); + + const isValidHash = + hashedString.length === LINEAGE_HASH_LENGTH && + !!hashedString.match(/^[0-9a-fA-F]+$/); + const isValidCounter1 = + lineageCounter1 >= 0 && lineageCounter1 <= LINEAGE_MAX_COUNTER_1; + const isValidCounter2 = + lineageCounter2 >= 0 && lineageCounter2 <= LINEAGE_MAX_COUNTER_2; + + return isValidHash && isValidCounter1 && isValidCounter2; + } + private static _parseTraceFlag(xraySampledFlag: string): TraceFlags | null { if (xraySampledFlag === NOT_SAMPLED) { return TraceFlags.NONE; diff --git a/propagators/propagator-aws-xray/test/AWSXRayPropagator.test.ts b/propagators/propagator-aws-xray/test/AWSXRayPropagator.test.ts index 4a6ab8db1c..4e22585abb 100644 --- a/propagators/propagator-aws-xray/test/AWSXRayPropagator.test.ts +++ b/propagators/propagator-aws-xray/test/AWSXRayPropagator.test.ts @@ -24,6 +24,7 @@ import { TraceFlags, trace, TextMapGetter, + propagation, } from '@opentelemetry/api'; import { TraceState } from '@opentelemetry/core'; @@ -33,6 +34,7 @@ describe('AWSXRayPropagator', () => { const xrayPropagator = new AWSXRayPropagator(); const TRACE_ID = '8a3c60f7d188f8fa79d48a391a778fa6'; const SPAN_ID = '53995c3f42cd8ad8'; + const LINEAGE_ID = '100:e3b0c442:11'; const SAMPLED_TRACE_FLAG = TraceFlags.SAMPLED; const NOT_SAMPLED_TRACE_FLAG = TraceFlags.NONE; @@ -119,6 +121,29 @@ describe('AWSXRayPropagator', () => { assert.deepStrictEqual(carrier, {}); }); + + it('should inject with lineage', () => { + const spanContext: SpanContext = { + traceId: TRACE_ID, + spanId: SPAN_ID, + traceFlags: SAMPLED_TRACE_FLAG, + }; + xrayPropagator.inject( + propagation.setBaggage( + trace.setSpan(ROOT_CONTEXT, trace.wrapSpanContext(spanContext)), + propagation.createBaggage({ + Lineage: { value: LINEAGE_ID }, + }) + ), + carrier, + defaultTextMapSetter + ); + + assert.deepStrictEqual( + carrier[AWSXRAY_TRACE_ID_HEADER], + 'Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Lineage=100:e3b0c442:11' + ); + }); }); describe('.extract()', () => { @@ -345,6 +370,51 @@ describe('AWSXRayPropagator', () => { }); }); + it('should extract lineage into baggage', () => { + carrier[AWSXRAY_TRACE_ID_HEADER] = + 'Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Lineage=100:e3b0c442:11'; + const extractedContext = xrayPropagator.extract( + ROOT_CONTEXT, + carrier, + defaultTextMapGetter + ); + + assert.deepStrictEqual( + propagation.getBaggage(extractedContext)?.getEntry('Lineage'), + { + value: LINEAGE_ID, + } + ); + }); + + const invalidLineageHeaders = [ + '', + '::', + '1::', + '1::1', + '1:badc0de:13', + ':fbadc0de:13', + '65535:fbadc0de:255', + ]; + + invalidLineageHeaders.forEach(lineageHeader => { + it(`should ignore invalid lineage header: ${lineageHeader}`, () => { + carrier[ + AWSXRAY_TRACE_ID_HEADER + ] = `Root=1-8a3c60f7-d188f8fa79d48a391a778fa6;Parent=53995c3f42cd8ad8;Sampled=1;Lineage=${lineageHeader}`; + const extractedContext = xrayPropagator.extract( + ROOT_CONTEXT, + carrier, + defaultTextMapGetter + ); + + assert.deepStrictEqual( + propagation.getBaggage(extractedContext), + undefined + ); + }); + }); + describe('.fields()', () => { it('should return a field with AWS X-Ray Trace ID header', () => { const expectedField = xrayPropagator.fields();