Repository: honeycombio/beeline-python

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**


Comments:

Patch:
diff --git a/beeline/__init__.py b/beeline/__init__.py
index e6dce92..ed238fc 100644
--- a/beeline/__init__.py
+++ b/beeline/__init__.py
@@ -9,7 +9,7 @@ from libhoney import Client
 from beeline.trace import SynchronousTracer
 from beeline.version import VERSION
 from beeline import internal
-import beeline.propagation.honeycomb
+import beeline.propagation.default
 import sys
 # pyflakes
 assert internal
@@ -59,8 +59,8 @@ class Beeline(object):
                  max_concurrent_batches=10, max_batch_size=100, send_frequency=0.25,
                  block_on_send=False, block_on_response=False,
                  transmission_impl=None, sampler_hook=None, presend_hook=None,
-                 http_trace_parser_hook=beeline.propagation.honeycomb.http_trace_parser_hook,
-                 http_trace_propagation_hook=beeline.propagation.honeycomb.http_trace_propagation_hook,
+                 http_trace_parser_hook=beeline.propagation.default.http_trace_parser_hook,
+                 http_trace_propagation_hook=beeline.propagation.default.http_trace_propagation_hook,
                  debug=False):
 
         self.client = None
diff --git a/beeline/propagation/default.py b/beeline/propagation/default.py
new file mode 100644
index 0000000..633eecc
--- /dev/null
+++ b/beeline/propagation/default.py
@@ -0,0 +1,21 @@
+from beeline.propagation import honeycomb, w3c
+
+
+def http_trace_parser_hook(request):
+    """
+    Retrieves the propagation context out of the request. Uses the honeycomb header, with W3C header as fallback.
+    """
+    honeycomb_header_value = honeycomb.http_trace_parser_hook(request)
+    w3c_header_value = w3c.http_trace_parser_hook(request)
+    if honeycomb_header_value:
+        return honeycomb_header_value
+    else:
+        return w3c_header_value
+
+
+def http_trace_propagation_hook(propagation_context):
+    """
+    Given a propagation context, returns a dictionary of key value pairs that should be
+    added to outbound requests (usually HTTP headers). Uses the honeycomb format.
+    """
+    return honeycomb.http_trace_propagation_hook(propagation_context)
diff --git a/beeline/trace.py b/beeline/trace.py
index 7f5561e..06a5bd5 100644
--- a/beeline/trace.py
+++ b/beeline/trace.py
@@ -18,6 +18,7 @@ from contextlib import contextmanager
 from beeline.internal import log, stringify_exception
 
 import beeline.propagation
+import beeline.propagation.default
 import beeline.propagation.honeycomb
 
 MAX_INT32 = math.pow(2, 32) - 1
@@ -49,8 +50,8 @@ class Tracer(object):
 
         self.presend_hook = None
         self.sampler_hook = None
-        self.http_trace_parser_hook = beeline.propagation.honeycomb.http_trace_parser_hook
-        self.http_trace_propagation_hook = beeline.propagation.honeycomb.http_trace_propagation_hook
+        self.http_trace_parser_hook = beeline.propagation.default.http_trace_parser_hook
+        self.http_trace_propagation_hook = beeline.propagation.default.http_trace_propagation_hook
 
     @contextmanager
     def __call__(self, name, trace_id=None, parent_id=None):


Test patch:
diff --git a/beeline/test_trace.py b/beeline/test_trace.py
index 43beece..5033618 100644
--- a/beeline/test_trace.py
+++ b/beeline/test_trace.py
@@ -623,7 +623,7 @@ class TestSpan(unittest.TestCase):
 
 
 class TestPropagationHooks(unittest.TestCase):
-    def test_propagate_and_start_trace(self):
+    def test_propagate_and_start_trace_uses_honeycomb_header(self):
         # FIXME: Test basics, including error handling and custom hooks
 
         # implicitly tests finish_span
@@ -653,6 +653,61 @@ class TestPropagationHooks(unittest.TestCase):
         # ensure that there is no current trace
         self.assertIsNone(tracer._trace)
 
+    def test_propagate_and_start_trace_uses_w3c_header_as_fallback(self):
+        # implicitly tests finish_span
+        m_client = Mock()
+        # these values are used before sending
+        m_client.new_event.return_value.start_time = datetime.datetime.now()
+        m_client.new_event.return_value.sample_rate = 1
+        tracer = SynchronousTracer(m_client)
+
+        header_value = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00'
+        req = DictRequest({
+            'traceparent': header_value,
+        })
+
+        span = tracer.propagate_and_start_trace(
+            context={'big': 'important_stuff'}, request=req)
+        self.assertEqual(tracer._trace.stack[0], span)
+        self.assertEqual(span.trace_id, "0af7651916cd43dd8448eb211c80319c")
+        self.assertEqual(span.parent_id, "b7ad6b7169203331")
+
+        tracer.finish_trace(span)
+
+        # ensure the event is sent
+        span.event.send_presampled.assert_called_once_with()
+        # ensure that there is no current trace
+        self.assertIsNone(tracer._trace)
+
+    def test_propagate_and_start_trace_uses_honeycomb_header_when_w3c_also_present(self):
+        # implicitly tests finish_span
+        m_client = Mock()
+        # these values are used before sending
+        m_client.new_event.return_value.start_time = datetime.datetime.now()
+        m_client.new_event.return_value.sample_rate = 1
+        tracer = SynchronousTracer(m_client)
+
+        w3c_value = '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-00'
+        honeycomb_value = '1;dataset=flibble,trace_id=bloop,parent_id=scoop,context=e30K'
+        req = DictRequest({
+            'traceparent': w3c_value,
+            'x-honeycomb-trace': honeycomb_value
+        })
+
+        span = tracer.propagate_and_start_trace(
+            context={'big': 'important_stuff'}, request=req)
+        self.assertEqual(tracer._trace.stack[0], span)
+        self.assertEqual(span.trace_id, "bloop")
+        self.assertEqual(span.parent_id, "scoop")
+
+        tracer.finish_trace(span)
+        self.assertEqual(span.event.dataset, 'flibble')
+
+        # ensure the event is sent
+        span.event.send_presampled.assert_called_once_with()
+        # ensure that there is no current trace
+        self.assertIsNone(tracer._trace)
+
     def test_error_handling(self):
         class TestException(Exception):
             pass