Repository: census-instrumentation/opencensus-python

Issue:
Trace header should be tied to propagator and not web framework
for example: https://github.com/census-instrumentation/opencensus-python/blob/master/opencensus/trace/ext/flask/flask_middleware.py#L34

This is related to https://github.com/census-instrumentation/opencensus-python/issues/46#issuecomment-384423270

Comments:

Patch:
diff --git a/opencensus/trace/propagation/google_cloud_format.py b/opencensus/trace/propagation/google_cloud_format.py
index 8c635dd..1e7b335 100644
--- a/opencensus/trace/propagation/google_cloud_format.py
+++ b/opencensus/trace/propagation/google_cloud_format.py
@@ -18,6 +18,7 @@ import re
 from opencensus.trace.span_context import SpanContext
 from opencensus.trace.trace_options import TraceOptions
 
+_TRACE_CONTEXT_HEADER_NAME = 'X_CLOUD_TRACE_CONTEXT'
 _TRACE_CONTEXT_HEADER_FORMAT = '([0-9a-f]{32})(\/([0-9a-f]{16}))?(;o=(\d+))?'
 _TRACE_CONTEXT_HEADER_RE = re.compile(_TRACE_CONTEXT_HEADER_FORMAT)
 _TRACE_ID_DELIMETER = '/'
@@ -73,6 +74,21 @@ class GoogleCloudFormatPropagator(object):
                 .format(header))
             return SpanContext()
 
+    def from_headers(self, headers):
+        """Generate a SpanContext object using the trace context header.
+
+        :type headers: dict
+        :param headers: HTTP request headers.
+
+        :rtype: :class:`~opencensus.trace.span_context.SpanContext`
+        :returns: SpanContext generated from the trace context header.
+        """
+        if headers is None:
+            return SpanContext()
+        if _TRACE_CONTEXT_HEADER_NAME not in headers:
+            return SpanContext()
+        return self.from_header(headers[_TRACE_CONTEXT_HEADER_NAME])
+
     def to_header(self, span_context):
         """Convert a SpanContext object to header string.
 
@@ -92,3 +108,17 @@ class GoogleCloudFormatPropagator(object):
             span_id,
             int(trace_options))
         return header
+
+    def to_headers(self, span_context):
+        """Convert a SpanContext object to HTTP request headers.
+
+        :type span_context:
+            :class:`~opencensus.trace.span_context.SpanContext`
+        :param span_context: SpanContext object.
+
+        :rtype: dict
+        :returns: Trace context headers in google cloud format.
+        """
+        return {
+            _TRACE_CONTEXT_HEADER_NAME: self.to_header(span_context),
+        }
diff --git a/opencensus/trace/propagation/trace_context_http_header_format.py b/opencensus/trace/propagation/trace_context_http_header_format.py
index 3ac3301..63a024c 100644
--- a/opencensus/trace/propagation/trace_context_http_header_format.py
+++ b/opencensus/trace/propagation/trace_context_http_header_format.py
@@ -18,6 +18,7 @@ import re
 from opencensus.trace.span_context import SpanContext
 from opencensus.trace.trace_options import TraceOptions
 
+_TRACE_PARENT_HEADER_NAME = 'traceparent'
 _TRACE_CONTEXT_HEADER_FORMAT = \
     '([0-9a-f]{2})(-([0-9a-f]{32}))(-([0-9a-f]{16}))?(-([0-9a-f]{2}))?'
 _TRACE_CONTEXT_HEADER_RE = re.compile(_TRACE_CONTEXT_HEADER_FORMAT)
@@ -76,6 +77,21 @@ class TraceContextPropagator(object):
 
         return SpanContext()
 
+    def from_headers(self, headers):
+        """Generate a SpanContext object using the W3C Distributed Tracing headers.
+
+        :type headers: dict
+        :param headers: HTTP request headers.
+
+        :rtype: :class:`~opencensus.trace.span_context.SpanContext`
+        :returns: SpanContext generated from the trace context header.
+        """
+        if headers is None:
+            return SpanContext()
+        if _TRACE_PARENT_HEADER_NAME not in headers:
+            return SpanContext()
+        return self.from_header(headers[_TRACE_PARENT_HEADER_NAME])
+
     def to_header(self, span_context):
         """Convert a SpanContext object to header string, using version 0.
 
@@ -98,3 +114,18 @@ class TraceContextPropagator(object):
             span_id,
             trace_options)
         return header
+
+    def to_headers(self, span_context):
+        """Convert a SpanContext object to W3C Distributed Tracing headers,
+        using version 0.
+
+        :type span_context:
+            :class:`~opencensus.trace.span_context.SpanContext`
+        :param span_context: SpanContext object.
+
+        :rtype: dict
+        :returns: W3C Distributed Tracing headers.
+        """
+        return {
+            _TRACE_PARENT_HEADER_NAME: self.to_header(span_context),
+        }


Test patch:
diff --git a/tests/unit/trace/propagation/test_google_cloud_format.py b/tests/unit/trace/propagation/test_google_cloud_format.py
index b18e7d4..626eca9 100644
--- a/tests/unit/trace/propagation/test_google_cloud_format.py
+++ b/tests/unit/trace/propagation/test_google_cloud_format.py
@@ -27,6 +27,22 @@ class TestGoogleCloudFormatPropagator(unittest.TestCase):
 
         assert isinstance(span_context, SpanContext)
 
+    def test_from_headers_none(self):
+        from opencensus.trace.span_context import SpanContext
+
+        propagator = google_cloud_format.GoogleCloudFormatPropagator()
+        span_context = propagator.from_headers(None)
+
+        assert isinstance(span_context, SpanContext)
+
+    def test_from_headers_empty(self):
+        from opencensus.trace.span_context import SpanContext
+
+        propagator = google_cloud_format.GoogleCloudFormatPropagator()
+        span_context = propagator.from_headers({})
+
+        assert isinstance(span_context, SpanContext)
+
     def test_header_type_error(self):
         header = 1234
 
@@ -81,6 +97,22 @@ class TestGoogleCloudFormatPropagator(unittest.TestCase):
 
         self.assertNotEqual(span_context.trace_id, trace_id)
 
+    def test_headers_match(self):
+        # Trace option is enabled.
+        headers = {
+            'X_CLOUD_TRACE_CONTEXT':
+                '6e0c63257de34c92bf9efcd03927272e/00f067aa0ba902b7;o=1',
+        }
+        expected_trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        expected_span_id = '00f067aa0ba902b7'
+
+        propagator = google_cloud_format.GoogleCloudFormatPropagator()
+        span_context = propagator.from_headers(headers)
+
+        self.assertEqual(span_context.trace_id, expected_trace_id)
+        self.assertEqual(span_context.span_id, expected_span_id)
+        self.assertTrue(span_context.trace_options.enabled)
+
     def test_to_header(self):
         from opencensus.trace import span_context
         from opencensus.trace import trace_options
@@ -99,3 +131,23 @@ class TestGoogleCloudFormatPropagator(unittest.TestCase):
             trace_id, span_id, 1)
 
         self.assertEqual(header, expected_header)
+
+    def test_to_headers(self):
+        from opencensus.trace import span_context
+        from opencensus.trace import trace_options
+
+        trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        span_id = '00f067aa0ba902b7'
+        span_context = span_context.SpanContext(
+            trace_id=trace_id,
+            span_id=span_id,
+            trace_options=trace_options.TraceOptions('1'))
+
+        propagator = google_cloud_format.GoogleCloudFormatPropagator()
+
+        headers = propagator.to_headers(span_context)
+        expected_headers = {
+            'X_CLOUD_TRACE_CONTEXT': '{}/{};o={}'.format(trace_id, span_id, 1),
+        }
+
+        self.assertEqual(headers, expected_headers)
diff --git a/tests/unit/trace/propagation/test_trace_context_http_header_format.py b/tests/unit/trace/propagation/test_trace_context_http_header_format.py
index dc75694..653d783 100644
--- a/tests/unit/trace/propagation/test_trace_context_http_header_format.py
+++ b/tests/unit/trace/propagation/test_trace_context_http_header_format.py
@@ -28,6 +28,24 @@ class TestTraceContextPropagator(unittest.TestCase):
 
         assert isinstance(span_context, SpanContext)
 
+    def test_from_headers_none(self):
+        from opencensus.trace.span_context import SpanContext
+
+        propagator = trace_context_http_header_format.\
+            TraceContextPropagator()
+        span_context = propagator.from_headers(None)
+
+        assert isinstance(span_context, SpanContext)
+
+    def test_from_headers_empty(self):
+        from opencensus.trace.span_context import SpanContext
+
+        propagator = trace_context_http_header_format.\
+            TraceContextPropagator()
+        span_context = propagator.from_headers({})
+
+        assert isinstance(span_context, SpanContext)
+
     def test_header_type_error(self):
         header = 1234
 
@@ -97,6 +115,23 @@ class TestTraceContextPropagator(unittest.TestCase):
 
         self.assertNotEqual(span_context.trace_id, trace_id)
 
+    def test_headers_match(self):
+        # Trace option is enabled.
+        headers = {
+            'traceparent':
+                '00-6e0c63257de34c92bf9efcd03927272e-00f067aa0ba902b7-01',
+        }
+        expected_trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        expected_span_id = '00f067aa0ba902b7'
+
+        propagator = trace_context_http_header_format.\
+            TraceContextPropagator()
+        span_context = propagator.from_headers(headers)
+
+        self.assertEqual(span_context.trace_id, expected_trace_id)
+        self.assertEqual(span_context.span_id, expected_span_id)
+        self.assertTrue(span_context.trace_options.enabled)
+
     def test_to_header(self):
         from opencensus.trace import span_context
         from opencensus.trace import trace_options
@@ -117,3 +152,24 @@ class TestTraceContextPropagator(unittest.TestCase):
             span_id_hex)
 
         self.assertEqual(header, expected_header)
+
+    def test_to_headers(self):
+        from opencensus.trace import span_context
+        from opencensus.trace import trace_options
+
+        trace_id = '6e0c63257de34c92bf9efcd03927272e'
+        span_id_hex = '00f067aa0ba902b7'
+        span_context = span_context.SpanContext(
+            trace_id=trace_id,
+            span_id=span_id_hex,
+            trace_options=trace_options.TraceOptions('1'))
+
+        propagator = trace_context_http_header_format.\
+            TraceContextPropagator()
+
+        headers = propagator.to_headers(span_context)
+        expected_headers = {
+            'traceparent': '00-{}-{}-01'.format(trace_id, span_id_hex),
+        }
+
+        self.assertEqual(headers, expected_headers)