Repository: honeycombio/beeline-nodejs
Pull request #187 (2020-03-19 19:49:35)

parse AWS trace header
We care about Root/Parent clauses currently, and map those to traceId/parentSpanId.

This code matches the behavior of code aws uses in their sdks.

We'll need to figure out how propagation works in the aws world (on outbound requests) so we can add our custom stuff there as well, which will in turn cause more changes to this code.

algorithm for dealing with the header is basically:

1. split aws header by `;`
2. walk the list of strings from #1, splitting each string by `=` into [key, value].
    2a. if we see key="Root", value becomes traceId.
    2b. if we see key="Parent", value becomes parentSpanId.
3. If we didn't see traceId, bail.
4. If we didn't see parentSpanId, fall back to traceId.



Patch:
diff --git a/lib/instrumentation/trace-util.js b/lib/instrumentation/trace-util.js
index b9f0446..4aea30e 100644
--- a/lib/instrumentation/trace-util.js
+++ b/lib/instrumentation/trace-util.js
@@ -21,11 +21,13 @@ const getValueFromHeaders = (requestHeaders, headers) => {
   return undefined;
 };
 
+const X_AMZN_TRACE_ID_HEADER = "X-Amzn-Trace-Id";
+
 exports.getTraceContext = (traceIdSource, req) => {
   if (typeof traceIdSource === "undefined" || typeof traceIdSource === "string") {
     let headers =
       typeof traceIdSource === "undefined"
-        ? [api.TRACE_HTTP_HEADER, "X-Request-ID", "X-Amzn-Trace-Id"]
+        ? [api.TRACE_HTTP_HEADER, "X-Request-ID", X_AMZN_TRACE_ID_HEADER]
         : [traceIdSource];
     let valueAndHeader = getValueFromHeaders(req.headers, headers);
 
@@ -45,6 +47,35 @@ exports.getTraceContext = (traceIdSource, req) => {
         });
       }
 
+      case X_AMZN_TRACE_ID_HEADER: {
+        let traceId, parentSpanId;
+
+        const split = value.split(";");
+        for (const s of split) {
+          const [name, value] = s.split("=");
+          if (name === "Root") {
+            traceId = value;
+          } else if (name === "Parent") {
+            parentSpanId = value;
+          }
+        }
+
+        if (!traceId) {
+          // if we didn't even get a 'Root=' clause, bail.
+          return {};
+        }
+
+        if (!parentSpanId) {
+          parentSpanId = traceId;
+        }
+
+        return {
+          traceId,
+          parentSpanId,
+          source: `${header} http header`,
+        };
+      }
+
       default: {
         return {
           traceId: value,


Test patch:
diff --git a/lib/instrumentation/express.test.js b/lib/instrumentation/express.test.js
index eca3872..9db6334 100644
--- a/lib/instrumentation/express.test.js
+++ b/lib/instrumentation/express.test.js
@@ -228,7 +228,7 @@ describe("request id from http headers", () => {
       .expect(200, () => {
         expect(api._apiForTesting().sentEvents.length).toBe(1);
         let ev = api._apiForTesting().sentEvents[0];
-        expect(ev[schema.TRACE_ID]).toBe("Root=1-67891233-abcdef012345678912345678");
+        expect(ev[schema.TRACE_ID]).toBe("1-67891233-abcdef012345678912345678");
         expect(ev[schema.TRACE_ID_SOURCE]).toBe("X-Amzn-Trace-Id http header");
         done();
       });
diff --git a/lib/instrumentation/fastify.test.js b/lib/instrumentation/fastify.test.js
index c658575..66704c9 100644
--- a/lib/instrumentation/fastify.test.js
+++ b/lib/instrumentation/fastify.test.js
@@ -185,7 +185,7 @@ describe("request id from http headers", () => {
       name: "X-Request-ID works",
       headers: [{ name: "X-Amzn-Trace-Id", value: "Root=1-67891233-abcdef012345678912345678" }],
       expectedHeaderName: "X-Amzn-Trace-Id",
-      expectedHeaderValue: "Root=1-67891233-abcdef012345678912345678",
+      expectedHeaderValue: "1-67891233-abcdef012345678912345678",
     },
 
     {
diff --git a/lib/instrumentation/trace-util.test.js b/lib/instrumentation/trace-util.test.js
new file mode 100644
index 0000000..1464c07
--- /dev/null
+++ b/lib/instrumentation/trace-util.test.js
@@ -0,0 +1,90 @@
+/* eslint-env node, jest */
+const cases = require("jest-in-case"),
+  http = require("http"),
+  api = require("../api"),
+  traceUtil = require("./trace-util");
+
+function getRequestWithHeader(name, value) {
+  const req = new http.IncomingMessage();
+  req.headers[name.toLowerCase()] = value;
+  return req;
+}
+
+describe("getTraceContext", () => {
+  cases(
+    "AWS X-Ray trace header",
+    opts => {
+      expect(
+        traceUtil.getTraceContext(
+          undefined,
+          getRequestWithHeader("X-Amzn-Trace-Id", opts.headerVal)
+        )
+      ).toEqual(opts.expectedContext);
+    },
+    [
+      {
+        name: "root / no parent",
+        headerVal: "Root=1-67891233-abcdef012345678912345678",
+        expectedContext: {
+          traceId: "1-67891233-abcdef012345678912345678",
+          parentSpanId: "1-67891233-abcdef012345678912345678",
+          source: "X-Amzn-Trace-Id 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: "self / root / no parent",
+        headerVal:
+          "Self=1-5983f5c9-36d365bc453d28036a63032b;Root=1-5983f5c9-56dcf0bc6d4d214d2dbbe8c6",
+        expectedContext: {
+          parentSpanId: "1-5983f5c9-56dcf0bc6d4d214d2dbbe8c6",
+          traceId: "1-5983f5c9-56dcf0bc6d4d214d2dbbe8c6",
+          source: "X-Amzn-Trace-Id 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",
+    opts => {
+      expect(
+        traceUtil.getTraceContext(
+          undefined,
+          getRequestWithHeader(api.TRACE_HTTP_HEADER, opts.headerVal)
+        )
+      ).toEqual(opts.expectedContext);
+    },
+    [
+      {
+        name: "v1 trace_id + parent_id, missing context",
+        headerVal: "1;trace_id=abcdef,parent_id=12345",
+        expectedContext: {
+          traceId: "abcdef",
+          parentSpanId: "12345",
+          customContext: undefined,
+          dataset: undefined,
+          source: "X-Honeycomb-Trace http header",
+        },
+      },
+      {
+        name: "v1, missing trace_id",
+        contextStr: "1;parent_id=12345",
+        expectedContext: {},
+      },
+    ]
+  );
+});