Repository: honeycombio/beeline-nodejs (js)

Issue:
Enable Beeline to simultaneously accept w3c and honeycomb propagation headers
<!---
Thank you for contributing an idea to this project!

Please see our [OSS process document](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md#) to get an idea of how we operate.
--->

**Is your feature request related to a problem? Please describe.**

Currently, it's not possible to incrementally move a microservices architecture from the honeycomb propagation headers format to the w3c propagation headers format. This makes it difficult if not impossible for beeline customers with a large enough deployment to move towards interoperating with OpenTelemetry.

**Describe the solution you'd like**

Allow the beeline to simultaneously accept both w3c and honeycomb propagation headers, whichever is found on the incoming request. The outgoing propagation header format can still be static and set by configuration.

**Describe alternatives you've considered**


**Additional context**


Pull request description:
feat: accept both w3c and honeycomb propagation headers by default
<!--
Thank you for contributing to the project! 💜
Please make sure to:
- Chat with us first if this is a big change
  - Open a new issue (or comment on an existing one)
  - We want to make sure you don't spend time implementing something we might have to say No to
- Add unit tests
- Mention any relevant issues in the PR description (e.g. "Fixes #123")

Please see our [OSS process document](https://github.com/honeycombio/home/blob/main/honeycomb-oss-lifecycle-and-practices.md#) to get an idea of how we operate.
-->

## Which problem is this PR solving?

- Closes #491 

## Short description of the changes

- Currently, a user needs to set `beeline.w3c.httpTraceParserHook` to properly handle distributed tracing across mixed environments of Beeline and OpenTelemetry. This change attempts to parse an incoming request as w3c even if no httpTraceParserHook is provided, by checking for existence of w3c header and falling back to that if found (instead of just falling back to honeycomb).
- If somehow both honeycomb and w3c headers are found, honeycomb is used and a warning is emitted when debug is enabled.

co-authored by @vreynolds and @robbkidd 

Patch:
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..7501f1a
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,10 @@
+version: "3"
+
+services:
+  pg:
+    image: postgres
+    environment:
+      POSTGRES_HOST_AUTH_METHOD: trust
+      POSTGRES_USER: ${USER}
+    ports:
+      - "127.0.0.1:5432:5432"
diff --git a/lib/instrumentation/trace-util.js b/lib/instrumentation/trace-util.js
index d884ce4..d65a0a3 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);


Test patch:
diff --git a/lib/instrumentation/trace-util.test.js b/lib/instrumentation/trace-util.test.js
index ddb1480..fa9bdac 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: {},
-      },
     ]
   );
 });