Skip to content

Commit a53000b

Browse files
rhilfersashishagg
authored andcommitted
Implement propagator options which allows for custom keys to be specified (#5)
* add tracer init to readme * added build and release features * fix makefile typo * can enable shared spans if desired * add ability to provide custom propagation headers if needed. default to Span-ID, Trace-Id, Parent-ID * fix lint issue
1 parent 56ad83e commit a53000b

File tree

5 files changed

+99
-38
lines changed

5 files changed

+99
-38
lines changed

examples/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from haystack import HaystackTracer
66
from haystack import AsyncHttpRecorder
77
from haystack import LoggerRecorder
8+
from haystack.text_propagator import TextPropagator
9+
from haystack.propagator import PropagatorOpts
810

911
recorder = LoggerRecorder()
1012
logging.basicConfig(level=logging.DEBUG)
@@ -15,6 +17,10 @@ def act_as_remote_service(headers):
1517
with HaystackTracer("Service-B", recorder,) as tracer:
1618
opentracing.tracer = tracer
1719

20+
# ---ability to use custom propagation headers if needed---
21+
# prop_opts = PropagatorOpts("X-Trace-ID", "X-Span-ID", "X-Parent-Span")
22+
# opentracing.tracer.register_propagator(opentracing.Format.HTTP_HEADERS, TextPropagator(prop_opts))
23+
1824
# simulate network transfer delay
1925
time.sleep(.25)
2026

haystack/constants.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
# Name of the HTTP header or Key used to encode Trace ID
2-
TRACE_ID_HEADER = "Trace-ID"
2+
TRACE_ID = "Trace-ID"
33

4-
# Name of the HTTP header or Key used to encode Span ID followed by any
5-
# alternate header names to extract as Span ID
6-
SPAN_ID_HEADERS = ("Span-ID", "Message-ID",)
4+
# Name of the HTTP header or Key used to encode Span ID
5+
SPAN_ID = "Span-ID"
76

8-
# Name of the HTTP header or Key used to encode Parent Span ID followed by any
9-
# alternate header names to extract as Parent Span ID
10-
PARENT_SPAN_ID_HEADERS = ("Parent-ID", "Parent-Message-ID",)
7+
# Name of the HTTP header or Key used to encode Parent Span ID
8+
PARENT_SPAN_ID = "Parent-ID"
119

1210
# Prefix of the HTTP header or Key used to encode Baggage items
13-
BAGGAGE_HEADER_PREFIX = "Baggage-"
11+
BAGGAGE_PREFIX = "Baggage-"
1412

1513
# The number of microseconds in one second
1614
SECONDS_TO_MICRO = 1000000

haystack/propagator.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
from abc import ABC, abstractmethod
2+
from collections import namedtuple
3+
from .constants import (
4+
TRACE_ID,
5+
SPAN_ID,
6+
PARENT_SPAN_ID,
7+
BAGGAGE_PREFIX,
8+
)
29

310

411
class Propagator(ABC):
@@ -10,3 +17,11 @@ def inject(self, span_context, carrier):
1017
@abstractmethod
1118
def extract(self, carrier):
1219
raise NotImplementedError()
20+
21+
22+
PropagatorOpts = namedtuple("PropagatorOpts", ["trace_id_key",
23+
"span_id_key",
24+
"parent_id_key",
25+
"baggage_key_prefix"])
26+
PropagatorOpts.__new__.__defaults__ = (TRACE_ID, SPAN_ID, PARENT_SPAN_ID,
27+
BAGGAGE_PREFIX)

haystack/text_propagator.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
from .propagator import Propagator
1+
from .propagator import Propagator, PropagatorOpts
22
from .span import SpanContext
33
from opentracing import SpanContextCorruptedException
4-
from .constants import (
5-
SPAN_ID_HEADERS,
6-
TRACE_ID_HEADER,
7-
PARENT_SPAN_ID_HEADERS,
8-
BAGGAGE_HEADER_PREFIX,
9-
)
104

115

126
class TextPropagator(Propagator):
137
"""A propagator for Format.TEXT_MAP and Format.HTTP_HEADERS"""
148

9+
def __init__(self, propagator_opts=None):
10+
"""
11+
Initialize with optional custom keys for the carrier.
12+
:param propagator_opts: PropagatorOps named tuple with custom keys
13+
to inject and extract :class:SpanContext from the carrier.
14+
"""
15+
self._propagator_opts = propagator_opts or PropagatorOpts()
16+
1517
def inject(self, span_context, carrier):
16-
carrier[TRACE_ID_HEADER] = span_context.trace_id
17-
carrier[SPAN_ID_HEADERS[0]] = span_context.span_id
18-
carrier[PARENT_SPAN_ID_HEADERS[0]] = span_context.parent_id
18+
carrier[self._propagator_opts.trace_id_key] = span_context.trace_id
19+
carrier[self._propagator_opts.span_id_key] = span_context.span_id
20+
carrier[self._propagator_opts.parent_id_key] = span_context.parent_id
1921
if span_context.baggage is not None:
2022
for item in span_context.baggage:
21-
carrier[BAGGAGE_HEADER_PREFIX + item] = \
23+
carrier[self._propagator_opts.baggage_key_prefix + item] = \
2224
span_context.baggage[item]
2325

2426
def extract(self, carrier):
@@ -27,16 +29,20 @@ def extract(self, carrier):
2729
parent_id = None
2830

2931
for key in carrier:
32+
lc_key = key.lower()
3033
value = carrier[key]
31-
if key in SPAN_ID_HEADERS:
34+
if lc_key == self._propagator_opts.span_id_key.lower():
3235
span_id = value
3336
count += 1
34-
elif key == TRACE_ID_HEADER:
37+
elif lc_key == self._propagator_opts.trace_id_key.lower():
3538
trace_id = value
3639
count += 1
37-
elif key.startswith(BAGGAGE_HEADER_PREFIX):
38-
baggage[key[len(BAGGAGE_HEADER_PREFIX):]] = value
39-
elif key in PARENT_SPAN_ID_HEADERS:
40+
elif lc_key.startswith(
41+
self._propagator_opts.baggage_key_prefix.lower()):
42+
baggage[
43+
key[len(self._propagator_opts.baggage_key_prefix):]] = \
44+
value
45+
elif lc_key == self._propagator_opts.parent_id_key.lower():
4046
parent_id = value
4147

4248
if count == 0:

tests/unit/test_text_propagator.py

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,76 @@
11
import unittest
22
from opentracing import SpanContextCorruptedException
33
from haystack.text_propagator import TextPropagator
4+
from haystack.propagator import PropagatorOpts
5+
from haystack.constants import (
6+
TRACE_ID,
7+
SPAN_ID,
8+
PARENT_SPAN_ID,
9+
BAGGAGE_PREFIX,
10+
)
411

512

613
class TextPropagatorTest(unittest.TestCase):
714

815
def setUp(self):
916
self.propagator = TextPropagator()
1017

11-
def test_message_id_in_carrier_should_be_extracted_as_span_id(self):
18+
def test_span_id_without_trace_id_throws_exception(self):
19+
carrier = {"Span-ID": "123456", "Foo-ID": "1234567"}
20+
21+
self.assertRaises(SpanContextCorruptedException,
22+
self.propagator.extract, carrier)
23+
24+
def test_trace_id_without_span_id_throws_exception(self):
25+
carrier = {"Trace-ID": "123456", "Foo-ID": "1234567"}
26+
27+
self.assertRaises(SpanContextCorruptedException,
28+
self.propagator.extract, carrier)
29+
30+
def test_no_context_in_carrier_returns_none(self):
31+
span_id = "1234"
32+
carrier = {"Not-A-Message-ID": span_id, "Not-A-Trace-ID": "123456"}
33+
34+
ctx = self.propagator.extract(carrier)
35+
36+
self.assertIsNone(ctx)
37+
38+
def test_lowercase_headers_are_still_extracted(self):
39+
pass
40+
41+
def test_default_propagator_options_injected_and_context_is_extracted(self):
1242
span_id = "1234"
1343
parent_id = "4321"
1444
trace_id = "1212"
15-
carrier = {"Message-ID": span_id, "Parent-Message-ID": parent_id,
16-
"Trace-ID": trace_id}
45+
baggage = {"Item1": "Value1", "Item2": "Value2"}
46+
carrier = {TRACE_ID: trace_id, SPAN_ID: span_id, PARENT_SPAN_ID: parent_id}
47+
for key, value in baggage.items():
48+
carrier[BAGGAGE_PREFIX + key] = value
1749

1850
ctx = self.propagator.extract(carrier)
1951

2052
self.assertEqual(ctx.span_id, span_id)
2153
self.assertEqual(ctx.parent_id, parent_id)
2254
self.assertEqual(ctx.trace_id, trace_id)
55+
self.assertDictEqual(ctx.baggage, baggage)
2356

24-
def test_corrupted_context_throws_exception(self):
57+
def test_custom_propagator_options_are_injected_and_context_is_extracted(self):
58+
propagator = TextPropagator(PropagatorOpts("X-Trace-ID", "X-Span-ID",
59+
"X-Parent-ID", "X-baggage-"))
2560
span_id = "1234"
26-
carrier = {"Message-ID": span_id, "Span-ID": "123456",
27-
"Trace-ID": "1234567"} # two span id's are invalid
28-
29-
self.assertRaises(SpanContextCorruptedException, self.propagator.extract, carrier)
30-
31-
def test_no_context_in_carrier_returns_none(self):
32-
span_id = "1234"
33-
carrier = {"Not-A-Message-ID": span_id, "Not-A-Trace-ID": "123456"}
61+
parent_id = "4321"
62+
trace_id = "1212"
63+
carrier = {"X-Span-ID": span_id, "X-Parent-ID": parent_id,
64+
"X-Trace-ID": trace_id}
65+
baggage = {"Item1": "Value1", "Item2": "Value2"}
66+
for key, value in baggage.items():
67+
carrier["X-baggage-" + key] = value
3468

35-
ctx = self.propagator.extract(carrier)
69+
ctx = propagator.extract(carrier)
3670

37-
self.assertIsNone(ctx)
71+
self.assertEqual(ctx.span_id, span_id)
72+
self.assertEqual(ctx.parent_id, parent_id)
73+
self.assertEqual(ctx.trace_id, trace_id)
3874

3975

4076
if __name__ == "__main__":

0 commit comments

Comments
 (0)