Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(propagator-aws-xray): support lineage in xray trace header #2679

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 79 additions & 14 deletions propagators/propagator-aws-xray/src/AWSXRayPropagator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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_REQUEST_COUNTER = 255;
const LINEAGE_MAX_LOOP_COUNTER = 32767;

/**
* Implementation of the AWS X-Ray Trace Header propagation protocol. See <a href=
* https://https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader>AWS
Expand All @@ -66,48 +73,74 @@ 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
? IS_SAMPLED
: 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic overall lgtm.

getContextFromHeader is probably not an accurate method name anymore after these changes.
Since all the extract logic is now here, I think it might make sense to just move all this up into the extract method.

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)
? rawTraceHeader[0]
: 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;
Expand All @@ -133,21 +166,32 @@ 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,
spanId: parsedSpanId,
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 {
Expand Down Expand Up @@ -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 requestCounter = parseInt(lineageSubstrings[0]);
const hashedResourceId = lineageSubstrings[1];
const loopCounter = parseInt(lineageSubstrings[2]);

const isValidKey =
hashedResourceId.length === LINEAGE_HASH_LENGTH &&
!!hashedResourceId.match(/^[0-9a-fA-F]+$/);
const isValidRequestCounter =
requestCounter >= 0 && requestCounter <= LINEAGE_MAX_REQUEST_COUNTER;
const isValidLoopCounter =
loopCounter >= 0 && loopCounter <= LINEAGE_MAX_LOOP_COUNTER;

return isValidKey && isValidRequestCounter && isValidLoopCounter;
}

private static _parseTraceFlag(xraySampledFlag: string): TraceFlags | null {
if (xraySampledFlag === NOT_SAMPLED) {
return TraceFlags.NONE;
Expand Down
70 changes: 70 additions & 0 deletions propagators/propagator-aws-xray/test/AWSXRayPropagator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
TraceFlags,
trace,
TextMapGetter,
propagation,
} from '@opentelemetry/api';
import { TraceState } from '@opentelemetry/core';

Expand All @@ -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;

Expand Down Expand Up @@ -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()', () => {
Expand Down Expand Up @@ -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();
Expand Down
Loading