From 6e3fc7bc289ba2c2f066eb7f09fd91cabca830a0 Mon Sep 17 00:00:00 2001 From: Andrey Slotin Date: Wed, 15 Jul 2020 20:03:45 +0200 Subject: [PATCH 1/4] Mark span context as synthetic if X-Instana-Synthetic is set to 1 --- instana/http_propagator.py | 16 ++++++++++++---- instana/span.py | 4 +++- tests/opentracing/test_ot_propagators.py | 4 +++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/instana/http_propagator.py b/instana/http_propagator.py index f75af4db..18a95578 100644 --- a/instana/http_propagator.py +++ b/instana/http_propagator.py @@ -28,6 +28,7 @@ class HTTPPropagator(): LC_HEADER_KEY_T = 'x-instana-t' LC_HEADER_KEY_S = 'x-instana-s' LC_HEADER_KEY_L = 'x-instana-l' + LC_HEADER_KEY_SYNTHETIC = 'x-instana-synthetic' ALT_HEADER_KEY_T = 'HTTP_X_INSTANA_T' ALT_HEADER_KEY_S = 'HTTP_X_INSTANA_S' @@ -35,6 +36,7 @@ class HTTPPropagator(): ALT_LC_HEADER_KEY_T = 'http_x_instana_t' ALT_LC_HEADER_KEY_S = 'http_x_instana_s' ALT_LC_HEADER_KEY_L = 'http_x_instana_l' + ALT_LC_HEADER_KEY_SYNTHETIC = 'http_x_instana_synthetic' def inject(self, span_context, carrier): try: @@ -63,6 +65,7 @@ def extract(self, carrier): # noqa trace_id = None span_id = None level = 1 + synthetic = False try: if type(carrier) is dict or hasattr(carrier, "__getitem__"): @@ -85,6 +88,8 @@ def extract(self, carrier): # noqa span_id = header_to_id(dc[key]) elif self.LC_HEADER_KEY_L == lc_key: level = dc[key] + elif self.LC_HEADER_KEY_SYNTHETIC == lc_key: + synthetic = dc[key] == "1" elif self.ALT_LC_HEADER_KEY_T == lc_key: trace_id = header_to_id(dc[key]) @@ -92,14 +97,17 @@ def extract(self, carrier): # noqa span_id = header_to_id(dc[key]) elif self.ALT_LC_HEADER_KEY_L == lc_key: level = dc[key] + elif self.ALT_LC_HEADER_KEY_SYNTHETIC == lc_key: + synthetic = dc[key] == "1" ctx = None if trace_id is not None and span_id is not None: ctx = SpanContext(span_id=span_id, - trace_id=trace_id, - level=level, - baggage={}, - sampled=True) + trace_id=trace_id, + level=level, + baggage={}, + sampled=True, + synthetic=synthetic) return ctx except Exception: diff --git a/instana/span.py b/instana/span.py index 3b347fa2..55eb64b6 100644 --- a/instana/span.py +++ b/instana/span.py @@ -13,12 +13,14 @@ def __init__( span_id=None, baggage=None, sampled=True, - level=1): + level=1, + synthetic=False): self.level = level self.trace_id = trace_id self.span_id = span_id self.sampled = sampled + self.synthetic = synthetic self._baggage = baggage or {} @property diff --git a/tests/opentracing/test_ot_propagators.py b/tests/opentracing/test_ot_propagators.py index 18e57911..8fa73079 100644 --- a/tests/opentracing/test_ot_propagators.py +++ b/tests/opentracing/test_ot_propagators.py @@ -50,12 +50,13 @@ def test_http_inject_with_list(): def test_http_basic_extract(): ot.tracer = InstanaTracer() - carrier = {'X-Instana-T': '1', 'X-Instana-S': '1', 'X-Instana-L': '1'} + carrier = {'X-Instana-T': '1', 'X-Instana-S': '1', 'X-Instana-L': '1', 'X-Instana-Synthetic': '1'} ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) assert isinstance(ctx, span.SpanContext) assert('0000000000000001' == ctx.trace_id) assert('0000000000000001' == ctx.span_id) + assert ctx.synthetic def test_http_mixed_case_extract(): @@ -67,6 +68,7 @@ def test_http_mixed_case_extract(): assert isinstance(ctx, span.SpanContext) assert('0000000000000001' == ctx.trace_id) assert('0000000000000001' == ctx.span_id) + assert not ctx.synthetic def test_http_no_context_extract(): From d1e3a6ef4cc923464da5210bfbfc05fe3fbe606a Mon Sep 17 00:00:00 2001 From: Andrey Slotin Date: Wed, 15 Jul 2020 20:13:48 +0200 Subject: [PATCH 2/4] Mark span as synthetic if the X-Instana-Synthetic header is set to 1 --- instana/span.py | 4 ++++ instana/tracer.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/instana/span.py b/instana/span.py index 55eb64b6..50e6983e 100644 --- a/instana/span.py +++ b/instana/span.py @@ -39,6 +39,7 @@ def with_baggage_item(self, key, value): class InstanaSpan(BasicSpan): stack = None + synthetic = False def finish(self, finish_time=None): super(InstanaSpan, self).finish(finish_time) @@ -172,6 +173,9 @@ def __init__(self, span, source, service_name, **kwargs): self.ec = span.tags.pop('ec', None) self.data = DictionaryOfStan() + if span.synthetic: + self.sy = True + if span.stack: self.stack = span.stack diff --git a/instana/tracer.py b/instana/tracer.py index 973576ed..4ce33d89 100644 --- a/instana/tracer.py +++ b/instana/tracer.py @@ -99,7 +99,8 @@ def start_span(self, context=ctx, parent_id=(None if parent_ctx is None else parent_ctx.span_id), tags=tags, - start_time=start_time) + start_time=start_time, + synthetic=(False if parent_ctx is None else parent_ctx.synthetic)) if operation_name in RegisteredSpan.EXIT_SPANS: self.__add_stack(span) From 482efdfdc58adee4e226f8d202dac4ecfc362c8b Mon Sep 17 00:00:00 2001 From: Andrey Slotin Date: Thu, 16 Jul 2020 11:55:19 +0200 Subject: [PATCH 3/4] Populate span.sy from X-Instana-Synthetic --- instana/span.py | 2 ++ instana/tracer.py | 6 +++-- tests/data/lambda/api_gateway_event.json | 5 +++-- tests/frameworks/test_aiohttp.py | 28 ++++++++++++++++++++++++ tests/frameworks/test_django.py | 26 ++++++++++++++++++++++ tests/frameworks/test_flask.py | 24 ++++++++++++++++++++ tests/frameworks/test_pyramid.py | 27 +++++++++++++++++++++++ tests/frameworks/test_tornado_server.py | 28 ++++++++++++++++++++++++ tests/frameworks/test_wsgi.py | 24 ++++++++++++++++++++ tests/platforms/test_lambda.py | 14 ++++++++++++ 10 files changed, 180 insertions(+), 4 deletions(-) diff --git a/instana/span.py b/instana/span.py index 50e6983e..43bab65a 100644 --- a/instana/span.py +++ b/instana/span.py @@ -157,6 +157,8 @@ def collect_logs(self): class BaseSpan(object): + sy = None + def __str__(self): return "BaseSpan(%s)" % self.__dict__.__str__() diff --git a/instana/tracer.py b/instana/tracer.py index 4ce33d89..434a48d8 100644 --- a/instana/tracer.py +++ b/instana/tracer.py @@ -99,8 +99,10 @@ def start_span(self, context=ctx, parent_id=(None if parent_ctx is None else parent_ctx.span_id), tags=tags, - start_time=start_time, - synthetic=(False if parent_ctx is None else parent_ctx.synthetic)) + start_time=start_time) + + if parent_ctx is not None: + span.synthetic = parent_ctx.synthetic if operation_name in RegisteredSpan.EXIT_SPANS: self.__add_stack(span) diff --git a/tests/data/lambda/api_gateway_event.json b/tests/data/lambda/api_gateway_event.json index 623d3dd2..23f54928 100644 --- a/tests/data/lambda/api_gateway_event.json +++ b/tests/data/lambda/api_gateway_event.json @@ -39,7 +39,8 @@ "X-Forwarded-Proto": "https", "X-Instana-T": "d5cb361b256413a9", "X-Instana-S": "0901d8ae4fbf1529", - "X-Instana-L": "1" + "X-Instana-L": "1", + "X-Instana-Synthetic": "1" }, "multiValueHeaders": { "Accept": [ @@ -132,4 +133,4 @@ "apiId": "1234567890", "protocol": "HTTP/1.1" } -} \ No newline at end of file +} diff --git a/tests/frameworks/test_aiohttp.py b/tests/frameworks/test_aiohttp.py index 5ac42357..64aa4c19 100644 --- a/tests/frameworks/test_aiohttp.py +++ b/tests/frameworks/test_aiohttp.py @@ -448,6 +448,11 @@ async def test(): self.assertEqual(aioclient_span.p, test_span.s) self.assertEqual(aioserver_span.p, aioclient_span.s) + # Synthetic + self.assertIsNone(test_span.sy) + self.assertIsNone(aioclient_span.sy) + self.assertIsNone(aioserver_span.sy) + # Error logging self.assertIsNone(test_span.ec) self.assertIsNone(aioclient_span.ec) @@ -478,6 +483,29 @@ async def test(): assert("Server-Timing" in response.headers) self.assertEqual(response.headers["Server-Timing"], "intid;desc=%s" % traceId) + def test_server_synthetic_request(self): + async def test(): + headers = { + 'X-Instana-Synthetic': '1' + } + + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["aiohttp_server"] + "/", headers=headers) + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + aioserver_span = spans[0] + aioclient_span = spans[1] + test_span = spans[2] + + self.assertTrue(aioserver_span.sy) + self.assertIsNone(aioclient_span.sy) + self.assertIsNone(test_span.sy) + def test_server_get_with_params_to_scrub(self): async def test(): with async_tracer.start_active_span('test'): diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index 52718c8e..7ae7b4dd 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -61,6 +61,10 @@ def test_basic_request(self): self.assertEqual(urllib3_span.p, test_span.s) self.assertEqual(django_span.p, urllib3_span.s) + self.assertIsNone(django_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + self.assertEqual(None, django_span.ec) self.assertEqual('/', django_span.data["http"]["url"]) @@ -69,6 +73,28 @@ def test_basic_request(self): assert django_span.stack self.assertEqual(2, len(django_span.stack)) + def test_synthetic_request(self): + headers = { + 'X-Instana-Synthetic': '1' + } + + with tracer.start_active_span('test'): + response = self.http.request('GET', self.live_server_url + '/', headers=headers) + + assert response + self.assertEqual(200, response.status) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + test_span = spans[2] + urllib3_span = spans[1] + django_span = spans[0] + + self.assertTrue(django_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + def test_request_with_error(self): with tracer.start_active_span('test'): response = self.http.request('GET', self.live_server_url + '/cause_error') diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 221cc9d7..6c1d1745 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -67,6 +67,11 @@ def test_get_request(self): self.assertEqual(urllib3_span.p, test_span.s) self.assertEqual(wsgi_span.p, urllib3_span.s) + # Synthetic + self.assertIsNone(wsgi_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + # Error logging self.assertIsNone(test_span.ec) self.assertIsNone(urllib3_span.ec) @@ -96,6 +101,25 @@ def test_get_request(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + def test_synthetic_request(self): + headers = { + 'X-Instana-Synthetic': '1' + } + + with tracer.start_active_span('test'): + response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + self.assertTrue(wsgi_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + def test_render_template(self): with tracer.start_active_span('test'): response = self.http.request('GET', testenv["wsgi_server"] + '/render') diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index fa13d9fe..3d4cb81e 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -65,6 +65,11 @@ def test_get_request(self): self.assertEqual(urllib3_span.p, test_span.s) self.assertEqual(pyramid_span.p, urllib3_span.s) + # Synthetic + self.assertIsNone(pyramid_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + # Error logging self.assertIsNone(test_span.ec) self.assertIsNone(urllib3_span.ec) @@ -95,6 +100,28 @@ def test_get_request(self): self.assertTrue(type(urllib3_span.stack) is list) self.assertTrue(len(urllib3_span.stack) > 1) + def test_synthetic_request(self): + headers = { + 'X-Instana-Synthetic': '1' + } + + with tracer.start_active_span('test'): + response = self.http.request('GET', testenv["pyramid_server"] + '/', headers=headers) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + pyramid_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + assert response + self.assertEqual(200, response.status) + + self.assertTrue(pyramid_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + def test_500(self): with tracer.start_active_span('test'): response = self.http.request('GET', testenv["pyramid_server"] + '/500') diff --git a/tests/frameworks/test_tornado_server.py b/tests/frameworks/test_tornado_server.py index e73e48b7..9620c945 100644 --- a/tests/frameworks/test_tornado_server.py +++ b/tests/frameworks/test_tornado_server.py @@ -73,6 +73,11 @@ async def test(): self.assertEqual(aiohttp_span.p, test_span.s) self.assertEqual(tornado_span.p, aiohttp_span.s) + # Synthetic + self.assertIsNone(tornado_span.sy) + self.assertIsNone(aiohttp_span.sy) + self.assertIsNone(test_span.sy) + # Error logging self.assertIsNone(test_span.ec) self.assertIsNone(aiohttp_span.ec) @@ -166,6 +171,29 @@ async def test(): self.assertTrue("Server-Timing" in response.headers) self.assertEqual(response.headers["Server-Timing"], "intid;desc=%s" % traceId) + def test_synthetic_request(self): + async def test(): + headers = { + 'X-Instana-Synthetic': '1' + } + + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["tornado_server"] + "/", headers=headers) + + response = tornado.ioloop.IOLoop.current().run_sync(test) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + tornado_span = get_first_span_by_name(spans, "tornado-server") + aiohttp_span = get_first_span_by_name(spans, "aiohttp-client") + test_span = get_first_span_by_name(spans, "sdk") + + self.assertTrue(tornado_span.sy) + self.assertIsNone(aiohttp_span.sy) + self.assertIsNone(test_span.sy) + def test_get_301(self): async def test(): with async_tracer.start_active_span('test'): diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index e92533c5..5f644bde 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -68,6 +68,10 @@ def test_get_request(self): self.assertEqual(urllib3_span.p, test_span.s) self.assertEqual(wsgi_span.p, urllib3_span.s) + self.assertIsNone(wsgi_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + # Error logging self.assertIsNone(test_span.ec) self.assertIsNone(urllib3_span.ec) @@ -83,6 +87,26 @@ def test_get_request(self): self.assertIsNotNone(wsgi_span.stack) self.assertEqual(2, len(wsgi_span.stack)) + def test_synthetic_request(self): + headers = { + 'X-Instana-Synthetic': '1' + } + with tracer.start_active_span('test'): + response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) + + spans = self.recorder.queued_spans() + + self.assertEqual(3, len(spans)) + self.assertIsNone(tracer.active_span) + + wsgi_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + self.assertTrue(wsgi_span.sy) + self.assertIsNone(urllib3_span.sy) + self.assertIsNone(test_span.sy) + def test_complex_request(self): with tracer.start_active_span('test'): response = self.http.request('GET', testenv["wsgi_server"] + '/complex') diff --git a/tests/platforms/test_lambda.py b/tests/platforms/test_lambda.py index b7d15faa..166b9b68 100644 --- a/tests/platforms/test_lambda.py +++ b/tests/platforms/test_lambda.py @@ -161,6 +161,8 @@ def test_custom_service_name(self): self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, span.f) + self.assertTrue(span.sy) + self.assertIsNone(span.ec) self.assertIsNone(span.data['lambda']['error']) @@ -219,6 +221,8 @@ def test_api_gateway_trigger_tracing(self): self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, span.f) + self.assertTrue(span.sy) + self.assertIsNone(span.ec) self.assertIsNone(span.data['lambda']['error']) @@ -276,6 +280,8 @@ def test_application_lb_trigger_tracing(self): self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, span.f) + self.assertTrue(span.sy) + self.assertIsNone(span.ec) self.assertIsNone(span.data['lambda']['error']) @@ -332,6 +338,8 @@ def test_cloudwatch_trigger_tracing(self): self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, span.f) + self.assertIsNone(span.sy) + self.assertIsNone(span.ec) self.assertIsNone(span.data['lambda']['error']) @@ -388,6 +396,8 @@ def test_cloudwatch_logs_trigger_tracing(self): self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, span.f) + self.assertIsNone(span.sy) + self.assertIsNone(span.ec) self.assertIsNone(span.data['lambda']['error']) @@ -446,6 +456,8 @@ def test_s3_trigger_tracing(self): self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, span.f) + self.assertIsNone(span.sy) + self.assertIsNone(span.ec) self.assertIsNone(span.data['lambda']['error']) @@ -503,6 +515,8 @@ def test_sqs_trigger_tracing(self): self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, span.f) + self.assertIsNone(span.sy) + self.assertIsNone(span.ec) self.assertIsNone(span.data['lambda']['error']) From 3f2bc2c5fcd699bced5dba0605e4e9b42a93c54c Mon Sep 17 00:00:00 2001 From: Andrey Slotin Date: Thu, 16 Jul 2020 16:45:50 +0200 Subject: [PATCH 4/4] Return an empty synthetic span context even if there was no trace context found --- instana/http_propagator.py | 3 +++ instana/tracer.py | 2 +- tests/clients/test_urllib3.py | 2 ++ tests/opentracing/test_ot_propagators.py | 12 ++++++++++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/instana/http_propagator.py b/instana/http_propagator.py index 18a95578..00dab833 100644 --- a/instana/http_propagator.py +++ b/instana/http_propagator.py @@ -108,6 +108,9 @@ def extract(self, carrier): # noqa baggage={}, sampled=True, synthetic=synthetic) + elif synthetic: + ctx = SpanContext(synthetic=synthetic) + return ctx except Exception: diff --git a/instana/tracer.py b/instana/tracer.py index 434a48d8..c47dfe25 100644 --- a/instana/tracer.py +++ b/instana/tracer.py @@ -84,7 +84,7 @@ def start_span(self, # Assemble the child ctx gid = generate_id() ctx = SpanContext(span_id=gid) - if parent_ctx is not None: + if parent_ctx is not None and parent_ctx.trace_id is not None: if parent_ctx._baggage is not None: ctx._baggage = parent_ctx._baggage.copy() ctx.trace_id = parent_ctx.trace_id diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index 02f21f76..c1f57c59 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -509,6 +509,8 @@ def test_client_error(self): self.assertEqual(1, urllib3_span.ec) def test_requestspkg_get(self): + self.recorder.clear_spans() + with tracer.start_active_span('test'): r = requests.get(testenv["wsgi_server"] + '/', timeout=2) diff --git a/tests/opentracing/test_ot_propagators.py b/tests/opentracing/test_ot_propagators.py index 8fa73079..0bd786f7 100644 --- a/tests/opentracing/test_ot_propagators.py +++ b/tests/opentracing/test_ot_propagators.py @@ -71,6 +71,18 @@ def test_http_mixed_case_extract(): assert not ctx.synthetic +def test_http_extract_synthetic_only(): + ot.tracer = InstanaTracer() + + carrier = {'X-Instana-Synthetic': '1'} + ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) + + assert isinstance(ctx, span.SpanContext) + assert ctx.trace_id is None + assert ctx.span_id is None + assert ctx.synthetic + + def test_http_no_context_extract(): ot.tracer = InstanaTracer()