From 07a2eacdee9733b210c8dbcbc12bdbeb372b463e Mon Sep 17 00:00:00 2001 From: JamieDanielson Date: Mon, 20 Dec 2021 09:45:35 -0500 Subject: [PATCH] feat: accept both w3c and honeycomb propagation headers by default (#514) * attempt to parse incoming header as w3c (even if no hook specified) if w3c header found - unless honeycomb header also found, then default to that. Co-authored-by: Robb Kidd --- lib/instrumentation/trace-util.js | 63 ++++++++++-------- lib/instrumentation/trace-util.test.js | 91 +++++++++++++++----------- 2 files changed, 89 insertions(+), 65 deletions(-) diff --git a/lib/instrumentation/trace-util.js b/lib/instrumentation/trace-util.js index d884ce4d..d65a0a30 100644 --- a/lib/instrumentation/trace-util.js +++ b/lib/instrumentation/trace-util.js @@ -1,26 +1,8 @@ /* eslint-env node */ -const api = require("../api"); -const propagation = require("../propagation"); - -// returns the header name/value for the first header with a value in the request. -const getValueFromHeaders = (requestHeaders, headers) => { - let value, header; - - for (const h of headers) { - let headerValue = requestHeaders[h.toLowerCase()]; - if (headerValue) { - header = h; // source = `${h} http header`; - value = headerValue; - break; - } - } - - if (typeof value !== "undefined") { - return { value, header }; - } - - return undefined; -}; +const api = require("../api"), + propagation = require("../propagation"), + pkg = require("../../package.json"), + debug = require("debug")(`${pkg.name}:event`); // adds trace source field to the context and returns the updated context function _includeSource(context, key) { @@ -56,16 +38,35 @@ exports.propagateTraceHeader = () => { // parse a trace header and return object used to populate args to startTrace // deprecated: in the next major version this should get renamed to getSpanContext exports.getTraceContext = (traceIdSource, req) => { - const { honeycomb, aws } = api; + const { honeycomb, w3c, aws } = api; if (typeof traceIdSource === "undefined" || typeof traceIdSource === "string") { - let headers = - typeof traceIdSource === "undefined" ? [honeycomb.TRACE_HTTP_HEADER] : [traceIdSource]; - let valueAndHeader = getValueFromHeaders(req.headers, headers); + let header, value; + + if (typeof traceIdSource !== "undefined") { + header = traceIdSource; + value = req.headers[traceIdSource.toLowerCase()]; + } else { + const honeycombHeaderValue = req.headers[honeycomb.TRACE_HTTP_HEADER.toLowerCase()]; + const w3cHeaderValue = req.headers[w3c.TRACE_HTTP_HEADER.toLowerCase()]; + + if (honeycombHeaderValue && w3cHeaderValue) { + debug("warn: received both honeycomb and w3c propagation headers, preferring honeycomb."); + } - if (!valueAndHeader) { + if (honeycombHeaderValue) { + header = honeycomb.TRACE_HTTP_HEADER; + value = honeycombHeaderValue; + } else if (w3cHeaderValue) { + header = w3c.TRACE_HTTP_HEADER; + value = w3cHeaderValue; + } + } + + if (!value) { + // couldn't find trace context return {}; } - let { value, header } = valueAndHeader; + let parsed = {}; switch (header) { //honeycomb trace header @@ -75,6 +76,12 @@ exports.getTraceContext = (traceIdSource, req) => { return _includeSource(parsed, header); } + case w3c.TRACE_HTTP_HEADER: { + header = w3c.TRACE_HTTP_HEADER; + parsed = w3c.unmarshalTraceContext(value); + return _includeSource(parsed, header); + } + case aws.TRACE_HTTP_HEADER: { header = aws.TRACE_HTTP_HEADER; parsed = aws.unmarshalTraceContext(value); diff --git a/lib/instrumentation/trace-util.test.js b/lib/instrumentation/trace-util.test.js index ddb1480b..fa9bdacc 100644 --- a/lib/instrumentation/trace-util.test.js +++ b/lib/instrumentation/trace-util.test.js @@ -10,81 +10,98 @@ function getRequestWithHeader(name, value) { return req; } +function getRequestWithHeaders(headers) { + const req = new http.IncomingMessage(); + headers.forEach((h) => (req.headers[h.name.toLowerCase()] = h.value)); + return req; +} + describe("getTraceContext", () => { cases( - "AWS X-Ray trace header", + "beeline trace header", (opts) => { expect( traceUtil.getTraceContext( - "X-Amzn-Trace-Id", - getRequestWithHeader("X-Amzn-Trace-Id", opts.headerVal) + undefined, + getRequestWithHeader(api.honeycomb.TRACE_HTTP_HEADER, opts.headerVal) ) ).toEqual(opts.expectedContext); }, [ { - name: "root / no parent", - headerVal: "Root=1-67891233-abcdef012345678912345678", + name: "v1 trace_id + parent_id, missing context", + headerVal: "1;trace_id=abcdef,parent_id=12345", expectedContext: { - traceId: "1-67891233-abcdef012345678912345678", - parentSpanId: "1-67891233-abcdef012345678912345678", - source: "X-Amzn-Trace-Id http header", + traceId: "abcdef", + parentSpanId: "12345", + customContext: undefined, + dataset: undefined, + source: "X-Honeycomb-Trace http header", }, }, { - name: "root / parent", - headerVal: "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8", - expectedContext: { - traceId: "1-5759e988-bd862e3fe1be46a994272793", - parentSpanId: "53995c3f42cd8ad8", - source: "X-Amzn-Trace-Id http header", - }, + name: "v1, missing trace_id", + contextStr: "1;parent_id=12345", + expectedContext: {}, }, + ] + ); + cases( + "w3c trace header fallback", + (opts) => { + expect( + traceUtil.getTraceContext( + undefined, + getRequestWithHeader(api.w3c.TRACE_HTTP_HEADER, opts.headerVal) + ) + ).toEqual(opts.expectedContext); + }, + [ { - name: "self / root / no parent", - headerVal: - "Parent=1-5983f5c9-36d365bc453d28036a63032b;Root=1-5983f5c9-56dcf0bc6d4d214d2dbbe8c6", + name: "w3c trace_id + parent_id, missing context", + headerVal: "00-7f042f75651d9782dcff93a45fa99be0-c998e73e5420f609-01", expectedContext: { - parentSpanId: "1-5983f5c9-36d365bc453d28036a63032b", - traceId: "1-5983f5c9-56dcf0bc6d4d214d2dbbe8c6", - source: "X-Amzn-Trace-Id http header", + traceId: "7f042f75651d9782dcff93a45fa99be0", + parentSpanId: "c998e73e5420f609", + customContext: undefined, + dataset: undefined, + source: "traceparent http header", }, }, - { - // shouldn't happen at least with aws generated headers, but if we're missing a Root= clause, we aren't in a trace at all. - name: "no root / parent", - headerVal: "Parent=53995c3f42cd8ad8", - expectedContext: {}, - }, ] ); cases( - "beeline trace header", + "if both honeycomb and w3c, choose honeycomb", (opts) => { expect( traceUtil.getTraceContext( undefined, - getRequestWithHeader(api.honeycomb.TRACE_HTTP_HEADER, opts.headerVal) + getRequestWithHeaders([ + { + name: api.w3c.TRACE_HTTP_HEADER, + value: opts.w3cHeaderVal, + }, + { + name: api.honeycomb.TRACE_HTTP_HEADER, + value: opts.beelineHeaderVal, + }, + ]) ) ).toEqual(opts.expectedContext); }, [ { - name: "v1 trace_id + parent_id, missing context", - headerVal: "1;trace_id=abcdef,parent_id=12345", + name: "we got both honeycomb and w3c, this should not happen", + w3cHeaderVal: "00-7f042f75651d9782dcff93a45fa99be0-c998e73e5420f609-01", + beelineHeaderVal: "1;trace_id=abcdefbeelineahh,parent_id=12345", expectedContext: { - traceId: "abcdef", + traceId: "abcdefbeelineahh", parentSpanId: "12345", customContext: undefined, dataset: undefined, source: "X-Honeycomb-Trace http header", }, }, - { - name: "v1, missing trace_id", - contextStr: "1;parent_id=12345", - expectedContext: {}, - }, ] ); });