Skip to content

Commit 0118318

Browse files
Ferenc-Andrey Slotin
andauthored
Handle X-INSTANA-L according to specifications (#352)
* feat(propagators): Properly propagate tracing level (X-INSTANA-L) * fix(level): Inject level and only return spans with level!=0 * fix(injection): Only inject trace_id and span_id when required * fix(recording): Don't record spans with suppression * fix(recording): Return early in case of suppression * fix(propagation): Avoid adding future trace context values accidentally * fix(propagation): Ensure that the context level is not overridden * test(flask): Add TC for suppression with w3c 'tracestate' * refactor(http_propagation): Remove duplication from carrier field population * fix(ci): Pin "responses" version * As stated in the changelog here: https://github.com/getsentry/responses/releases/tag/0.18.0 0.18.0 Removed internal `_matches` attribute of RequestsMock object. Which has been used by our UT TCs, so before we refactor that we need to pin the last known working version. * feat(span_context): Use a dedicated property for signaling suppression * feat(propagator): Use suppression property in base_propagator For skipping tracestate update in suppression mode Co-authored-by: Andrey Slotin <[email protected]> Co-authored-by: Andrey Slotin <[email protected]>
1 parent 8ce957e commit 0118318

File tree

10 files changed

+104
-49
lines changed

10 files changed

+104
-49
lines changed

instana/instrumentation/flask/vanilla.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
def before_request_with_instana(*argv, **kwargs):
2121
try:
2222
env = flask.request.environ
23-
ctx = None
24-
2523
ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env)
2624

2725
flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx)

instana/propagators/base_propagator.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,22 @@ def _get_participating_trace_context(self, span_context):
130130
traceparent = span_context.traceparent
131131
tracestate = span_context.tracestate
132132
traceparent = self._tp.update_traceparent(traceparent, tp_trace_id, span_context.span_id, span_context.level)
133+
134+
# In suppression mode do not update the tracestate and
135+
# do not add the 'in=' key-value pair to the incoming tracestate
136+
# Just propagate the incoming tracestate (if any) unchanged.
137+
if span_context.suppression:
138+
return traceparent, tracestate
139+
133140
tracestate = self._ts.update_tracestate(tracestate, span_context.trace_id, span_context.span_id)
134141
return traceparent, tracestate
135142

136143
def __determine_span_context(self, trace_id, span_id, level, synthetic, traceparent, tracestate,
137144
disable_w3c_trace_context):
138145
"""
139146
This method determines the span context depending on a set of conditions being met
140-
Detailed description of the conditions can be found here:
141-
https://github.com/instana/technical-documentation/tree/master/tracing/specification#http-processing-for-instana-tracers
147+
Detailed description of the conditions can be found in the instana internal technical-documentation,
148+
under section http-processing-for-instana-tracers
142149
:param trace_id: instana trace id
143150
:param span_id: instana span id
144151
:param level: instana level
@@ -157,11 +164,15 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar
157164
correlation = True
158165

159166
ctx_level = self._get_ctx_level(level)
167+
if ctx_level == 0 or level == '0':
168+
trace_id = ctx.trace_id = None
169+
span_id = ctx.span_id = None
170+
ctx.correlation_type = None
171+
ctx.correlation_id = None
160172

161173
if trace_id and span_id:
162174
ctx.trace_id = trace_id[-16:] # only the last 16 chars
163175
ctx.span_id = span_id[-16:] # only the last 16 chars
164-
ctx.level = ctx_level
165176
ctx.synthetic = synthetic is not None
166177

167178
if len(trace_id) > 16:
@@ -176,7 +187,6 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar
176187
if disable_traceparent == "":
177188
ctx.trace_id = tp_trace_id[-16:]
178189
ctx.span_id = tp_parent_id
179-
ctx.level = ctx_level
180190
ctx.synthetic = synthetic is not None
181191
ctx.trace_parent = True
182192
ctx.instana_ancestor = instana_ancestor
@@ -185,7 +195,6 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar
185195
if instana_ancestor:
186196
ctx.trace_id = instana_ancestor.t
187197
ctx.span_id = instana_ancestor.p
188-
ctx.level = ctx_level
189198
ctx.synthetic = synthetic is not None
190199

191200
elif synthetic:
@@ -198,6 +207,8 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar
198207
ctx.traceparent = traceparent
199208
ctx.tracestate = tracestate
200209

210+
ctx.level = ctx_level
211+
201212
return ctx
202213

203214
def __extract_instana_headers(self, dc):
@@ -263,7 +274,7 @@ def __extract_w3c_trace_context_headers(self, dc):
263274

264275
def extract(self, carrier, disable_w3c_trace_context=False):
265276
"""
266-
This method overrides the one of the Baseclass as with the introduction of W3C trace context for the HTTP
277+
This method overrides one of the Baseclasses as with the introduction of W3C trace context for the HTTP
267278
requests more extracting steps and logic was required
268279
:param disable_w3c_trace_context:
269280
:param carrier:
@@ -288,4 +299,4 @@ def extract(self, carrier, disable_w3c_trace_context=False):
288299

289300
return ctx
290301
except Exception:
291-
logger.debug("extract error:", exc_info=True)
302+
logger.debug("extract error:", exc_info=True)

instana/propagators/http_propagator.py

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,49 +19,36 @@ def __init__(self):
1919
super(HTTPPropagator, self).__init__()
2020

2121
def inject(self, span_context, carrier, disable_w3c_trace_context=False):
22-
try:
23-
trace_id = span_context.trace_id
24-
span_id = span_context.span_id
25-
level = span_context.level
26-
27-
if disable_w3c_trace_context:
28-
traceparent, tracestate = [None] * 2
29-
else:
30-
traceparent, tracestate = self._get_participating_trace_context(span_context)
31-
32-
if isinstance(carrier, dict) or hasattr(carrier, "__dict__"):
33-
if traceparent and tracestate:
34-
carrier[self.HEADER_KEY_TRACEPARENT] = traceparent
35-
carrier[self.HEADER_KEY_TRACESTATE] = tracestate
36-
carrier[self.HEADER_KEY_T] = trace_id
37-
carrier[self.HEADER_KEY_S] = span_id
38-
carrier[self.HEADER_KEY_L] = "1"
39-
elif isinstance(carrier, list):
40-
if traceparent and tracestate:
41-
carrier.append((self.HEADER_KEY_TRACEPARENT, traceparent))
42-
carrier.append((self.HEADER_KEY_TRACESTATE, tracestate))
43-
carrier.append((self.HEADER_KEY_T, trace_id))
44-
carrier.append((self.HEADER_KEY_S, span_id))
45-
carrier.append((self.HEADER_KEY_L, "1"))
46-
elif hasattr(carrier, '__setitem__'):
47-
if traceparent and tracestate:
48-
carrier.__setitem__(self.HEADER_KEY_TRACEPARENT, traceparent)
49-
carrier.__setitem__(self.HEADER_KEY_TRACESTATE, tracestate)
50-
carrier.__setitem__(self.HEADER_KEY_T, trace_id)
51-
carrier.__setitem__(self.HEADER_KEY_S, span_id)
52-
carrier.__setitem__(self.HEADER_KEY_L, "1")
22+
trace_id = span_context.trace_id
23+
span_id = span_context.span_id
24+
serializable_level = str(span_context.level)
25+
26+
if disable_w3c_trace_context:
27+
traceparent, tracestate = [None] * 2
28+
else:
29+
traceparent, tracestate = self._get_participating_trace_context(span_context)
30+
31+
def inject_key_value(carrier, key, value):
32+
if isinstance(carrier, list):
33+
carrier.append((key, value))
34+
elif isinstance(carrier, dict) or '__setitem__' in dir(carrier):
35+
carrier[key] = value
5336
else:
5437
raise Exception("Unsupported carrier type", type(carrier))
5538

56-
except Exception:
57-
logger.debug("inject error:", exc_info=True)
58-
59-
60-
61-
62-
63-
39+
try:
40+
inject_key_value(carrier, self.HEADER_KEY_L, serializable_level)
6441

42+
if traceparent:
43+
inject_key_value(carrier, self.HEADER_KEY_TRACEPARENT, traceparent)
44+
if tracestate:
45+
inject_key_value(carrier, self.HEADER_KEY_TRACESTATE, tracestate)
6546

47+
if span_context.suppression:
48+
return
6649

50+
inject_key_value(carrier, self.HEADER_KEY_T, trace_id)
51+
inject_key_value(carrier, self.HEADER_KEY_S, span_id)
6752

53+
except Exception:
54+
logger.debug("inject error:", exc_info=True)

instana/recorder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ def record_span(self, span):
7575
"""
7676
Convert the passed BasicSpan into and add it to the span queue
7777
"""
78+
if span.context.suppression:
79+
return
80+
7881
if self.agent.can_send():
7982
service_name = None
8083
source = self.agent.get_from_structure()

instana/span.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ def __init__(self, span, source, service_name, **kwargs):
106106
self.t = span.context.trace_id
107107
self.p = span.parent_id
108108
self.s = span.context.span_id
109+
self.l = span.context.level
109110
self.ts = int(round(span.start_time * 1000))
110111
self.d = int(round(span.duration * 1000))
111112
self.f = source

instana/span_context.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,16 @@ def correlation_id(self, value):
8888
def baggage(self):
8989
return self._baggage
9090

91+
@property
92+
def suppression(self):
93+
return self.level == 0
94+
9195
def with_baggage_item(self, key, value):
9296
new_baggage = self._baggage.copy()
9397
new_baggage[key] = value
9498
return SpanContext(
9599
trace_id=self.trace_id,
96100
span_id=self.span_id,
97101
sampled=self.sampled,
98-
baggage=new_baggage)
102+
level=self.level,
103+
baggage=new_baggage)

instana/tracer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def start_span(self,
9292
ctx.long_trace_id = parent_ctx.long_trace_id
9393
ctx.trace_parent = parent_ctx.trace_parent
9494
ctx.instana_ancestor = parent_ctx.instana_ancestor
95+
ctx.level = parent_ctx.level
9596
ctx.correlation_type = parent_ctx.correlation_type
9697
ctx.correlation_id = parent_ctx.correlation_id
9798
ctx.traceparent = parent_ctx.traceparent
@@ -100,6 +101,7 @@ def start_span(self,
100101
ctx.trace_id = gid
101102
ctx.sampled = self.sampler.sampled(ctx.trace_id)
102103
if parent_ctx is not None:
104+
ctx.level = parent_ctx.level
103105
ctx.correlation_type = parent_ctx.correlation_type
104106
ctx.correlation_id = parent_ctx.correlation_id
105107
ctx.traceparent = parent_ctx.traceparent

tests/frameworks/test_flask.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,52 @@ def test_get_request(self):
102102
# We should NOT have a path template for this route
103103
self.assertIsNone(wsgi_span.data["http"]["path_tpl"])
104104

105+
def test_get_request_with_suppression(self):
106+
headers = {'X-INSTANA-L':'0'}
107+
response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers)
108+
109+
spans = self.recorder.queued_spans()
110+
111+
self.assertEqual(response.headers.get('X-INSTANA-L', None), '0')
112+
# The traceparent has to be present
113+
self.assertIsNotNone(response.headers.get('traceparent', None))
114+
# The last digit of the traceparent has to be 0
115+
self.assertEqual(response.headers['traceparent'][-1], '0')
116+
117+
# This should not be present
118+
self.assertIsNone(response.headers.get('tracestate', None))
119+
120+
# Assert that there isn't any span, where level is not 0!
121+
self.assertFalse(any(map(lambda x: x.l != 0, spans)))
122+
123+
# Assert that there are no spans in the recorded list
124+
self.assertEquals(spans, [])
125+
126+
def test_get_request_with_suppression_and_w3c(self):
127+
headers = {
128+
'X-INSTANA-L':'0',
129+
'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01',
130+
'tracestate': 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7'}
131+
132+
response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers)
133+
134+
spans = self.recorder.queued_spans()
135+
136+
self.assertEqual(response.headers.get('X-INSTANA-L', None), '0')
137+
self.assertIsNotNone(response.headers.get('traceparent', None))
138+
self.assertEqual(response.headers['traceparent'][-1], '0')
139+
# The tracestate has to be present
140+
self.assertIsNotNone(response.headers.get('tracestate', None))
141+
142+
# The 'in=' section can not be in the tracestate
143+
self.assertTrue('in=' not in response.headers['tracestate'])
144+
145+
# Assert that there isn't any span, where level is not 0!
146+
self.assertFalse(any(map(lambda x: x.l != 0, spans)))
147+
148+
# Assert that there are no spans in the recorded list
149+
self.assertEquals(spans, [])
150+
105151
def test_synthetic_request(self):
106152
headers = {
107153
'X-INSTANA-SYNTHETIC': '1'

tests/requirements-310.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pytest>=6.2.4
3535
pytest-celery
3636
redis>=3.5.3
3737
requests-mock
38+
responses<=0.17.0
3839
sanic>=19.0.0,<21.9.0
3940
sqlalchemy>=1.4.15
4041
spyne>=2.13.16

tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pytest>=6.2.4
2323
pytest-celery
2424
redis>=3.5.3
2525
requests-mock
26+
responses<=0.17.0
2627
sanic>=19.0.0,<21.9.0
2728
sqlalchemy>=1.4.15
2829
spyne>=2.13.16

0 commit comments

Comments
 (0)