From cd6059a547382ad466c695503ff70c9b1cff0c93 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 14 Mar 2024 17:14:13 +0100 Subject: [PATCH 001/172] feat(OTel): Initial commit to migrate to OpenTelemetry. setup.py configuration to replace the dependency of OpenTracing with OpenTelemetry (OTel) and bump up to version 3.0.0. Signed-off-by: Paulo Vital --- pyproject.toml | 5 ++--- src/instana/version.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2703ace0..1d225f3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ requires-python = ">=3.8" license = "MIT" keywords = [ "performance", - "opentracing", + "opentelemetry", "metrics", "monitoring", "tracing", @@ -44,13 +44,12 @@ classifiers = [ ] dependencies = [ "autowrapt>=1.0", - "basictracer>=3.1.0", "fysom>=2.1.2", - "opentracing>=2.3.0", "protobuf<5.0.0", "requests>=2.6.0", "six>=1.12.0", "urllib3>=1.26.5", + "opentelemetry-api>=1.23.0", ] [project.entry-points."instana"] diff --git a/src/instana/version.py b/src/instana/version.py index 36a74f5b..ee874de0 100644 --- a/src/instana/version.py +++ b/src/instana/version.py @@ -3,4 +3,4 @@ # Module version file. Used by setup.py and snapshot reporting. -VERSION = "2.5.2" +VERSION = "3.0.0.dev0" From d27de2b0eef22a0c8cbe5759c8ac48a1b726e35b Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 21 Mar 2024 22:07:22 +0100 Subject: [PATCH 002/172] test(OTel): Remove opentracing tests and disable tests. Disabled all tests that are not necessary in the beginning of the migration. Signed-off-by: Paulo Vital --- tests/conftest.py | 9 +- tests/opentracing/__init__.py | 0 tests/opentracing/test_opentracing.py | 28 -- tests/opentracing/test_ot_propagators.py | 309 ----------------------- tests/opentracing/test_ot_span.py | 290 --------------------- tests/opentracing/test_ot_tracer.py | 10 - 6 files changed, 8 insertions(+), 638 deletions(-) delete mode 100644 tests/opentracing/__init__.py delete mode 100644 tests/opentracing/test_opentracing.py delete mode 100644 tests/opentracing/test_ot_propagators.py delete mode 100644 tests/opentracing/test_ot_span.py delete mode 100644 tests/opentracing/test_ot_tracer.py diff --git a/tests/conftest.py b/tests/conftest.py index d63dea98..8086edc3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,14 @@ # Make sure the instana package is fully loaded import instana -collect_ignore_glob = [] +collect_ignore_glob = [ + "*autoprofile*", + "*clients*", + "*frameworks*", + "*platforms*", + "*propagators*", + "*w3c_trace_context*", +] # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will # be run explicitly. (So always exclude them here) diff --git a/tests/opentracing/__init__.py b/tests/opentracing/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/opentracing/test_opentracing.py b/tests/opentracing/test_opentracing.py deleted file mode 100644 index 0ed9e508..00000000 --- a/tests/opentracing/test_opentracing.py +++ /dev/null @@ -1,28 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -from unittest import SkipTest -from opentracing.harness.api_check import APICompatibilityCheckMixin - -from instana.tracer import InstanaTracer - - -class TestInstanaTracer(InstanaTracer, APICompatibilityCheckMixin): - def tracer(self): - return self - - def test_binary_propagation(self): - raise SkipTest('Binary format is not supported') - - def test_mandatory_formats(self): - raise SkipTest('Binary format is not supported') - - def check_baggage_values(self): - return True - - def is_parent(self, parent, span): - # use `Span` ids to check parenting - if parent is None: - return span.parent_id is None - - return parent.context.span_id == span.parent_id diff --git a/tests/opentracing/test_ot_propagators.py b/tests/opentracing/test_ot_propagators.py deleted file mode 100644 index de26f471..00000000 --- a/tests/opentracing/test_ot_propagators.py +++ /dev/null @@ -1,309 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import inspect -import unittest - -import opentracing as ot - -import instana.propagators.http_propagator as ihp -import instana.propagators.text_propagator as itp -import instana.propagators.binary_propagator as ibp -from instana.span_context import SpanContext -from instana.tracer import InstanaTracer - - -class TestOTSpan(unittest.TestCase): - def test_http_basics(self): - inspect.isclass(ihp.HTTPPropagator) - - inject_func = getattr(ihp.HTTPPropagator, "inject", None) - self.assertTrue(inject_func) - self.assertTrue(callable(inject_func)) - - extract_func = getattr(ihp.HTTPPropagator, "extract", None) - self.assertTrue(extract_func) - self.assertTrue(callable(extract_func)) - - - def test_http_inject_with_dict(self): - ot.tracer = InstanaTracer() - - carrier = {} - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.HTTP_HEADERS, carrier) - - self.assertIn('X-INSTANA-T', carrier) - self.assertEqual(carrier['X-INSTANA-T'], span.context.trace_id) - self.assertIn('X-INSTANA-S', carrier) - self.assertEqual(carrier['X-INSTANA-S'], span.context.span_id) - self.assertIn('X-INSTANA-L', carrier) - self.assertEqual(carrier['X-INSTANA-L'], "1") - - - def test_http_inject_with_list(self): - ot.tracer = InstanaTracer() - - carrier = [] - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.HTTP_HEADERS, carrier) - - self.assertIn(('X-INSTANA-T', span.context.trace_id), carrier) - self.assertIn(('X-INSTANA-S', span.context.span_id), carrier) - self.assertIn(('X-INSTANA-L', "1"), carrier) - - - def test_http_basic_extract(self): - ot.tracer = InstanaTracer() - - 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) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_http_extract_with_byte_keys(self): - ot.tracer = InstanaTracer() - - carrier = {b'X-INSTANA-T': '1', b'X-INSTANA-S': '1', b'X-INSTANA-L': '1', b'X-INSTANA-SYNTHETIC': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_http_extract_from_list_of_tuples(self): - ot.tracer = InstanaTracer() - - carrier = [(b'user-agent', b'python-requests/2.23.0'), (b'accept-encoding', b'gzip, deflate'), - (b'accept', b'*/*'), (b'connection', b'keep-alive'), - (b'x-instana-t', b'1'), (b'x-instana-s', b'1'), (b'x-instana-l', b'1'), (b'X-INSTANA-SYNTHETIC', '1')] - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_http_mixed_case_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertFalse(ctx.synthetic) - - - def test_http_extract_synthetic_only(self): - ot.tracer = InstanaTracer() - - carrier = {'X-INSTANA-SYNTHETIC': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertTrue(ctx.synthetic) - - - def test_http_default_context_extract(self): - ot.tracer = InstanaTracer() - - carrier = {} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertFalse(ctx.synthetic) - - def test_http_128bit_headers(self): - ot.tracer = InstanaTracer() - - carrier = {'X-INSTANA-T': '0000000000000000b0789916ff8f319f', - 'X-INSTANA-S': '0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} - ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, 'b0789916ff8f319f') - self.assertEqual(ctx.span_id, 'b0789916ff8f319f') - - - def test_text_basics(self): - inspect.isclass(itp.TextPropagator) - - inject_func = getattr(itp.TextPropagator, "inject", None) - self.assertTrue(inject_func) - self.assertTrue(callable(inject_func)) - - extract_func = getattr(itp.TextPropagator, "extract", None) - self.assertTrue(extract_func) - self.assertTrue(callable(extract_func)) - - - def test_text_inject_with_dict(self): - ot.tracer = InstanaTracer() - - carrier = {} - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.TEXT_MAP, carrier) - - self.assertIn('x-instana-t', carrier) - self.assertEqual(carrier['x-instana-t'], span.context.trace_id) - self.assertIn('x-instana-s', carrier) - self.assertEqual(carrier['x-instana-s'], span.context.span_id) - self.assertIn('x-instana-l', carrier) - self.assertEqual(carrier['x-instana-l'], "1") - - - def test_text_inject_with_list(self): - ot.tracer = InstanaTracer() - - carrier = [] - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.TEXT_MAP, carrier) - - self.assertIn(('x-instana-t', span.context.trace_id), carrier) - self.assertIn(('x-instana-s', span.context.span_id), carrier) - self.assertIn(('x-instana-l', "1"), carrier) - - - def test_text_basic_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-instana-t': '1', 'x-instana-s': '1', 'x-instana-l': '1'} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - - - def test_text_mixed_case_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1'} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - - - def test_text_default_context_extract(self): - ot.tracer = InstanaTracer() - - carrier = {} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertFalse(ctx.synthetic) - - - def test_text_128bit_headers(self): - ot.tracer = InstanaTracer() - - carrier = {'x-instana-t': '0000000000000000b0789916ff8f319f', - 'x-instana-s': ' 0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} - ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, 'b0789916ff8f319f') - self.assertEqual(ctx.span_id, 'b0789916ff8f319f') - - def test_binary_basics(self): - inspect.isclass(ibp.BinaryPropagator) - - inject_func = getattr(ibp.BinaryPropagator, "inject", None) - self.assertTrue(inject_func) - self.assertTrue(callable(inject_func)) - - extract_func = getattr(ibp.BinaryPropagator, "extract", None) - self.assertTrue(extract_func) - self.assertTrue(callable(extract_func)) - - - def test_binary_inject_with_dict(self): - ot.tracer = InstanaTracer() - - carrier = {} - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.BINARY, carrier) - - self.assertIn(b'x-instana-t', carrier) - self.assertEqual(carrier[b'x-instana-t'], str.encode(span.context.trace_id)) - self.assertIn(b'x-instana-s', carrier) - self.assertEqual(carrier[b'x-instana-s'], str.encode(span.context.span_id)) - self.assertIn(b'x-instana-l', carrier) - self.assertEqual(carrier[b'x-instana-l'], b'1') - - - def test_binary_inject_with_list(self): - ot.tracer = InstanaTracer() - - carrier = [] - span = ot.tracer.start_span("unittest") - ot.tracer.inject(span.context, ot.Format.BINARY, carrier) - - self.assertIn((b'x-instana-t', str.encode(span.context.trace_id)), carrier) - self.assertIn((b'x-instana-s', str.encode(span.context.span_id)), carrier) - self.assertIn((b'x-instana-l', b'1'), carrier) - - - def test_binary_basic_extract(self): - ot.tracer = InstanaTracer() - - carrier = {b'X-INSTANA-T': b'1', b'X-INSTANA-S': b'1', b'X-INSTANA-L': b'1', b'X-INSTANA-SYNTHETIC': b'1'} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_binary_mixed_case_extract(self): - ot.tracer = InstanaTracer() - - carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1', b'X-inStaNa-SYNtheTIC': b'1'} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, '0000000000000001') - self.assertEqual(ctx.span_id, '0000000000000001') - self.assertTrue(ctx.synthetic) - - - def test_binary_default_context_extract(self): - ot.tracer = InstanaTracer() - - carrier = {} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertIsNone(ctx.trace_id) - self.assertIsNone(ctx.span_id) - self.assertFalse(ctx.synthetic) - - - def test_binary_128bit_headers(self): - ot.tracer = InstanaTracer() - - carrier = {'X-INSTANA-T': '0000000000000000b0789916ff8f319f', - 'X-INSTANA-S': ' 0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} - ctx = ot.tracer.extract(ot.Format.BINARY, carrier) - - self.assertIsInstance(ctx, SpanContext) - self.assertEqual(ctx.trace_id, 'b0789916ff8f319f') - self.assertEqual(ctx.span_id, 'b0789916ff8f319f') diff --git a/tests/opentracing/test_ot_span.py b/tests/opentracing/test_ot_span.py deleted file mode 100644 index 9280df76..00000000 --- a/tests/opentracing/test_ot_span.py +++ /dev/null @@ -1,290 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import re -import sys -import json -import time -import unittest -from uuid import UUID - -import opentracing - -from instana.util import to_json -from instana.singletons import agent, tracer -from ..helpers import get_first_span_by_filter - - -class TestOTSpan(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - agent.options.service_name = None - opentracing.tracer = tracer - recorder = opentracing.tracer.recorder - recorder.clear_spans() - - def tearDown(self): - """ Do nothing for now """ - return None - - def test_span_interface(self): - span = opentracing.tracer.start_span("blah") - self.assertTrue(hasattr(span, "finish")) - self.assertTrue(hasattr(span, "set_tag")) - self.assertTrue(hasattr(span, "tags")) - self.assertTrue(hasattr(span, "operation_name")) - self.assertTrue(hasattr(span, "set_baggage_item")) - self.assertTrue(hasattr(span, "get_baggage_item")) - self.assertTrue(hasattr(span, "context")) - self.assertTrue(hasattr(span, "log")) - - def test_span_ids(self): - count = 0 - while count <= 1000: - count += 1 - span = opentracing.tracer.start_span("test_span_ids") - context = span.context - self.assertTrue(0 <= int(context.span_id, 16) <= 18446744073709551615) - self.assertTrue(0 <= int(context.trace_id, 16) <= 18446744073709551615) - - # Python 3.11 support is incomplete yet - # TODO: Remove this once we find a workaround or DROP opentracing! - @unittest.skipIf(sys.version_info >= (3, 11), reason="Raises not Implemented exception in OSX") - def test_stacks(self): - # Entry spans have no stack attached by default - wsgi_span = opentracing.tracer.start_span("wsgi") - self.assertIsNone(wsgi_span.stack) - - # SDK spans have no stack attached by default - sdk_span = opentracing.tracer.start_span("unregistered_span_type") - self.assertIsNone(sdk_span.stack) - - # Exit spans are no longer than 30 frames - exit_span = opentracing.tracer.start_span("urllib3") - self.assertLessEqual(len(exit_span.stack), 30) - - def test_span_fields(self): - span = opentracing.tracer.start_span("mycustom") - self.assertEqual("mycustom", span.operation_name) - self.assertTrue(span.context) - - span.set_tag("tagone", "string") - span.set_tag("tagtwo", 150) - - self.assertEqual("string", span.tags['tagone']) - self.assertEqual(150, span.tags['tagtwo']) - - @unittest.skipIf(sys.platform == "darwin", reason="Raises not Implemented exception in OSX") - def test_span_queueing(self): - recorder = opentracing.tracer.recorder - - count = 1 - while count <= 20: - count += 1 - span = opentracing.tracer.start_span("queuethisplz") - span.set_tag("tagone", "string") - span.set_tag("tagtwo", 150) - span.finish() - - self.assertEqual(20, recorder.queue_size()) - - def test_sdk_spans(self): - recorder = opentracing.tracer.recorder - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag("tagone", "string") - span.set_tag("tagtwo", 150) - span.set_tag('span.kind', "entry") - time.sleep(0.5) - span.finish() - - spans = recorder.queued_spans() - self.assertEqual(1, len(spans)) - - sdk_span = spans[0] - self.assertEqual('sdk', sdk_span.n) - self.assertEqual(None, sdk_span.p) - self.assertEqual(sdk_span.s, sdk_span.t) - self.assertTrue(sdk_span.ts) - self.assertGreater(sdk_span.ts, 0) - self.assertTrue(sdk_span.d) - self.assertGreater(sdk_span.d, 0) - - self.assertTrue(sdk_span.data) - self.assertTrue(sdk_span.data["sdk"]) - self.assertEqual('entry', sdk_span.data["sdk"]["type"]) - self.assertEqual('custom_sdk_span', sdk_span.data["sdk"]["name"]) - self.assertTrue(sdk_span.data["sdk"]["custom"]) - self.assertTrue(sdk_span.data["sdk"]["custom"]["tags"]) - - def test_span_kind(self): - recorder = opentracing.tracer.recorder - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "consumer") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "server") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "producer") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "client") - span.finish() - - span = opentracing.tracer.start_span("custom_sdk_span") - span.set_tag('span.kind', "blah") - span.finish() - - spans = recorder.queued_spans() - self.assertEqual(5, len(spans)) - - span = spans[0] - self.assertEqual('entry', span.data["sdk"]["type"]) - - span = spans[1] - self.assertEqual('entry', span.data["sdk"]["type"]) - - span = spans[2] - self.assertEqual('exit', span.data["sdk"]["type"]) - - span = spans[3] - self.assertEqual('exit', span.data["sdk"]["type"]) - - span = spans[4] - self.assertEqual('intermediate', span.data["sdk"]["type"]) - - span = spans[0] - self.assertEqual(1, span.k) - - span = spans[1] - self.assertEqual(1, span.k) - - span = spans[2] - self.assertEqual(2, span.k) - - span = spans[3] - self.assertEqual(2, span.k) - - span = spans[4] - self.assertEqual(3, span.k) - - def test_tag_values(self): - with tracer.start_active_span('test') as scope: - # Set a UUID class as a tag - # If unchecked, this causes a json.dumps error: "ValueError: Circular reference detected" - scope.span.set_tag('uuid', UUID(bytes=b'\x12\x34\x56\x78'*4)) - # Arbitrarily setting an instance of some class - scope.span.set_tag('tracer', tracer) - scope.span.set_tag('none', None) - scope.span.set_tag('mylist', [1, 2, 3]) - scope.span.set_tag('myset', {"one", 2}) - - spans = tracer.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - test_span = spans[0] - self.assertTrue(test_span) - self.assertEqual(len(test_span.data['sdk']['custom']['tags']), 5) - self.assertEqual(test_span.data['sdk']['custom']['tags']['uuid'], "UUID('12345678-1234-5678-1234-567812345678')") - self.assertTrue(test_span.data['sdk']['custom']['tags']['tracer']) - self.assertEqual(test_span.data['sdk']['custom']['tags']['none'], 'None') - self.assertListEqual(test_span.data['sdk']['custom']['tags']['mylist'], [1, 2, 3]) - self.assertRegex(test_span.data['sdk']['custom']['tags']['myset'], r"\{.*,.*\}") - - # Convert to JSON - json_data = to_json(test_span) - self.assertTrue(json_data) - - # And back - span_dict = json.loads(json_data) - self.assertEqual(len(span_dict['data']['sdk']['custom']['tags']), 5) - self.assertEqual(span_dict['data']['sdk']['custom']['tags']['uuid'], "UUID('12345678-1234-5678-1234-567812345678')") - self.assertTrue(span_dict['data']['sdk']['custom']['tags']['tracer']) - self.assertEqual(span_dict['data']['sdk']['custom']['tags']['none'], 'None') - self.assertListEqual(span_dict['data']['sdk']['custom']['tags']['mylist'], [1, 2, 3]) - self.assertRegex(test_span.data['sdk']['custom']['tags']['myset'], r"{.*,.*}") - - def test_tag_names(self): - with tracer.start_active_span('test') as scope: - # Tag names (keys) must be strings - scope.span.set_tag(1234567890, 'This should not get set') - # Unicode key name - scope.span.set_tag(u'asdf', 'This should be ok') - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 1) - - test_span = spans[0] - self.assertTrue(test_span) - self.assertEqual(len(test_span.data['sdk']['custom']['tags']), 1) - self.assertEqual(test_span.data['sdk']['custom']['tags']['asdf'], 'This should be ok') - - json_data = to_json(test_span) - self.assertTrue(json_data) - - def test_custom_service_name(self): - # Set a custom service name - agent.options.service_name = "custom_service_name" - - with tracer.start_active_span('entry_span') as scope: - scope.span.set_tag('span.kind', 'server') - scope.span.set_tag(u'type', 'entry_span') - - with tracer.start_active_span('intermediate_span', child_of=scope.span) as exit_scope: - exit_scope.span.set_tag(u'type', 'intermediate_span') - - with tracer.start_active_span('exit_span', child_of=scope.span) as exit_scope: - exit_scope.span.set_tag('span.kind', 'client') - exit_scope.span.set_tag(u'type', 'exit_span') - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == "entry_span" - entry_span = get_first_span_by_filter(spans, filter) - self.assertTrue(entry_span) - - filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == "intermediate_span" - intermediate_span = get_first_span_by_filter(spans, filter) - self.assertTrue(intermediate_span) - - filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == "exit_span" - exit_span = get_first_span_by_filter(spans, filter) - self.assertTrue(exit_span) - - self.assertTrue(entry_span) - self.assertEqual(len(entry_span.data['sdk']['custom']['tags']), 2) - self.assertEqual(entry_span.data['sdk']['custom']['tags']['type'], 'entry_span') - self.assertEqual(entry_span.data['service'], 'custom_service_name') - self.assertEqual(entry_span.k, 1) - - self.assertTrue(intermediate_span) - self.assertEqual(len(intermediate_span.data['sdk']['custom']['tags']), 1) - self.assertEqual(intermediate_span.data['sdk']['custom']['tags']['type'], 'intermediate_span') - self.assertEqual(intermediate_span.data['service'], 'custom_service_name') - self.assertEqual(intermediate_span.k, 3) - - self.assertTrue(exit_span) - self.assertEqual(len(exit_span.data['sdk']['custom']['tags']), 2) - self.assertEqual(exit_span.data['sdk']['custom']['tags']['type'], 'exit_span') - self.assertEqual(exit_span.data['service'], 'custom_service_name') - self.assertEqual(exit_span.k, 2) - - def test_span_log(self): - with tracer.start_active_span('mylogspan') as scope: - scope.span.log_kv({'Don McLean': 'American Pie'}) - scope.span.log_kv({'Elton John': 'Your Song'}) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 1) - - my_log_span = spans[0] - self.assertEqual(my_log_span.n, 'sdk') - - log_data = my_log_span.data['sdk']['custom']['logs'] - self.assertEqual(len(log_data), 2) diff --git a/tests/opentracing/test_ot_tracer.py b/tests/opentracing/test_ot_tracer.py deleted file mode 100644 index c73037f4..00000000 --- a/tests/opentracing/test_ot_tracer.py +++ /dev/null @@ -1,10 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import opentracing - - -def test_tracer_basics(): - assert hasattr(opentracing.tracer, "start_span") - assert hasattr(opentracing.tracer, "inject") - assert hasattr(opentracing.tracer, "extract") From f285f84b7f56e35972271f25562e1d778bb33abc Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 21 Mar 2024 22:08:27 +0100 Subject: [PATCH 003/172] ci(OTel): Adapt CircleCI to OTel and set to run only on master branch. Signed-off-by: Paulo Vital --- .circleci/config.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a3027569..41e885cb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -78,6 +78,7 @@ commands: steps: - store_test_results: path: test-results + run_sonarqube: steps: - attach_workspace: @@ -118,7 +119,7 @@ commands: jobs: python38: docker: - - image: cimg/python:3.8.20 + - image: cimg/python:3.8 - image: cimg/postgres:9.6.24 environment: POSTGRES_USER: root @@ -142,7 +143,7 @@ jobs: python39: docker: - - image: cimg/python:3.9.20 + - image: cimg/python:3.9 - image: cimg/postgres:9.6.24 environment: POSTGRES_USER: root @@ -166,7 +167,7 @@ jobs: python310: docker: - - image: cimg/python:3.10.15 + - image: cimg/python:3.10 - image: cimg/postgres:9.6.24 environment: POSTGRES_USER: root @@ -191,7 +192,7 @@ jobs: python311: docker: - - image: cimg/python:3.11.10 + - image: cimg/python:3.11 - image: cimg/postgres:9.6.24 environment: POSTGRES_USER: root @@ -231,7 +232,7 @@ jobs: python312: docker: - - image: cimg/python:3.12.6 + - image: cimg/python:3.12 - image: cimg/postgres:9.6.24 environment: POSTGRES_USER: root @@ -296,7 +297,7 @@ jobs: py39couchbase: docker: - - image: cimg/python:3.9.20 + - image: cimg/python:3.9 - image: couchbase/server-sandbox:5.5.0 working_directory: ~/repo steps: @@ -312,7 +313,7 @@ jobs: py39cassandra: docker: - - image: cimg/python:3.9.20 + - image: cimg/python:3.9 - image: cassandra:3.11 environment: MAX_HEAP_SIZE: 2048m From 585dd56aba1b8b7fb8a932b37c6191f59ae7790e Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 25 Mar 2024 14:51:54 +0530 Subject: [PATCH 004/172] refactor(span): Span and SpanContext migration to use OTel. - Implement all the abstract methods provided by the OTel API - Adapt the existing code to OTel conventions. - Inherit OTel's SpanContext. - Comment out the usage of baggage. - Use time in nano seconds for start_time and end_time. - Initialization of Span's attributes, events, start_time, status, duration, and synthetic. - Add the duration, status, and parent_id properties. - Minor fixes to set values or get a dictionary value. Co-authored-by: Paulo Vital Signed-off-by: Varsha GS --- src/instana/span.py | 945 +++++++++++++++++++++++------------- src/instana/span_context.py | 46 +- 2 files changed, 644 insertions(+), 347 deletions(-) diff --git a/src/instana/span.py b/src/instana/span.py index c9896453..3c7f820b 100644 --- a/src/instana/span.py +++ b/src/instana/span.py @@ -4,7 +4,7 @@ """ This module contains the classes that represents spans. -InstanaSpan - the OpenTracing based span used during tracing +InstanaSpan - the OpenTelemetry based span used during tracing When an InstanaSpan is finished, it is converted into either an SDKSpan or RegisteredSpan depending on type. @@ -14,102 +14,276 @@ - RegisteredSpan: Class that represents a Registered type span """ import six +from typing import Dict, Optional, Union, Sequence, Tuple +from threading import Lock +from time import time_ns -from basictracer.span import BasicSpan -import opentracing.ext.tags as ot_tags +from opentelemetry.trace import Span # , SpanContext +from opentelemetry.util import types +from opentelemetry.trace.status import Status, StatusCode +from .span_context import SpanContext from .log import logger from .util import DictionaryOfStan -class InstanaSpan(BasicSpan): +class Event: + def __init__( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + self._name = name + self._attributes = attributes + if timestamp is None: + self._timestamp = time_ns() + else: + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def timestamp(self) -> int: + return self._timestamp + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class InstanaSpan(Span): stack = None synthetic = False - def mark_as_errored(self, tags=None): + def __init__( + self, + name: str, + context: SpanContext, + parent_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + attributes: types.Attributes = {}, + events: Sequence[Event] = [], + status: Optional[Status] = Status(StatusCode.UNSET), + ) -> None: + self._name = name + self._context = context + self._lock = Lock() + self._start_time = start_time or time_ns() + self._end_time = end_time + self._duration = 0 + self._attributes = attributes + self._events = events + self._parent_id = parent_id + self._status = status + + if context.synthetic: + self.synthetic = True + + + @property + def name(self) -> str: + return self._name + + def get_span_context(self) -> SpanContext: + return self._context + + @property + def context(self) -> SpanContext: + return self._context + + @property + def start_time(self) -> Optional[int]: + return self._start_time + + @property + def end_time(self) -> Optional[int]: + return self._end_time + + @property + def duration(self) -> int: + return self._duration + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + def set_attributes(self, attributes: Dict[str, types.AttributeValue]) -> None: + if not self._attributes: + self._attributes = {} + + with self._lock: + for key, value in attributes.items(): + self._attributes[key] = value + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + return self.set_attributes({key: value}) + + @property + def events(self) -> Sequence[Event]: + return self._events + + @property + def status(self) -> Status: + return self._status + + @property + def parent_id(self) -> int: + return self._parent_id + + def update_name(self, name: str) -> None: + with self._lock: + self._name = name + + def is_recording(self) -> bool: + return self._end_time is None + + def set_status( + self, + status: Union[Status, StatusCode], + description: Optional[str] = None, + ) -> None: + # Ignore future calls if status is already set to OK + # Ignore calls to set to StatusCode.UNSET + if isinstance(status, Status): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return + if description is not None: + logger.warning( + "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", + description, + ) + self._status = status + elif isinstance(status, StatusCode): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status is StatusCode.UNSET + ): + return + self._status = Status(status, description) + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + + event = Event( + name=name, + attributes=attributes, + timestamp=timestamp, + ) + + self._events.append(event) + + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + escaped: bool = False, + ) -> None: + """ + Records an exception as a span event. This will record pertinent info from the exception and + assure that this span is marked as errored. + """ + try: + message = "" + self.mark_as_errored() + if hasattr(exception, "__str__") and len(str(exception)) > 0: + message = str(exception) + elif hasattr(exception, "message") and exception.message is not None: + message = exception.message + else: + message = repr(exception) + + if self.name in ["rpc-server", "rpc-client"]: + self.set_attribute("rpc.error", message) + elif self.name == "mysql": + self.set_attribute("mysql.error", message) + elif self.name == "postgres": + self.set_attribute("pg.error", message) + elif self.name in RegisteredSpan.HTTP_SPANS: + self.set_attribute("http.error", message) + elif self.name in ["celery-client", "celery-worker"]: + self.set_attribute("error", message) + elif self.name == "sqlalchemy": + self.set_attribute("sqlalchemy.err", message) + elif self.name == "aws.lambda.entry": + self.set_attribute("lambda.error", message) + else: + _attributes = {"message": message} + if attributes: + _attributes.update(attributes) + self.add_event( + name="exception", attributes=_attributes, timestamp=timestamp + ) + except Exception: + logger.debug("span.record_exception", exc_info=True) + raise + + def end(self, end_time: Optional[int] = None) -> None: + with self._lock: + self._end_time = end_time if end_time is not None else time_ns() + self._duration = self._end_time - self._start_time + + def mark_as_errored(self, attributes: types.Attributes = None) -> None: """ Mark this span as errored. - @param tags: optional tags to add to the span + @param attributes: optional attributes to add to the span """ try: - ec = self.tags.get('ec', 0) - self.set_tag('ec', ec + 1) + ec = self.attributes.get("ec", 0) + self.set_attribute("ec", ec + 1) - if tags is not None and isinstance(tags, dict): - for key in tags: - self.set_tag(key, tags[key]) + if attributes is not None and isinstance(attributes, dict): + for key in attributes: + self.set_attribute(key, attributes[key]) except Exception: - logger.debug('span.mark_as_errored', exc_info=True) + logger.debug("span.mark_as_errored", exc_info=True) - def assure_errored(self): + def assure_errored(self) -> None: """ Make sure that this span is marked as errored. @return: None """ try: - ec = self.tags.get('ec', None) + ec = self.attributes.get("ec", None) if ec is None or ec == 0: - self.set_tag('ec', 1) + self.set_attribute("ec", 1) except Exception: - logger.debug('span.assure_errored', exc_info=True) - - def log_exception(self, exc): - """ - Log an exception onto this span. This will log pertinent info from the exception and - assure that this span is marked as errored. - - @param e: the exception to log - """ - try: - message = "" - self.mark_as_errored() - if hasattr(exc, '__str__') and len(str(exc)) > 0: - message = str(exc) - elif hasattr(exc, 'message') and exc.message is not None: - message = exc.message - else: - message = repr(exc) - - if self.operation_name in ['rpc-server', 'rpc-client']: - self.set_tag('rpc.error', message) - elif self.operation_name == "mysql": - self.set_tag('mysql.error', message) - elif self.operation_name == "postgres": - self.set_tag('pg.error', message) - elif self.operation_name in RegisteredSpan.HTTP_SPANS: - self.set_tag('http.error', message) - elif self.operation_name in ["celery-client", "celery-worker"]: - self.set_tag('error', message) - elif self.operation_name == "sqlalchemy": - self.set_tag('sqlalchemy.err', message) - elif self.operation_name == "aws.lambda.entry": - self.set_tag('lambda.error', message) - else: - self.log_kv({'message': message}) - except Exception: - logger.debug("span.log_exception", exc_info=True) - raise + logger.debug("span.assure_errored", exc_info=True) class BaseSpan(object): sy = None - def __str__(self): + def __str__(self) -> str: return "BaseSpan(%s)" % self.__dict__.__str__() - def __repr__(self): + def __repr__(self) -> str: return self.__dict__.__str__() - def __init__(self, span, source, service_name, **kwargs): + def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name self.t = span.context.trace_id self.p = span.parent_id + # self.p = span.context.span_id if span.context.is_remote else None self.s = span.context.span_id - self.ts = int(round(span.start_time * 1000)) - self.d = int(round(span.duration * 1000)) + self.ts = round(span.start_time / 10**6) + self.d = round(span.duration / 10**6) self.f = source - self.ec = span.tags.pop('ec', None) + self.ec = span.attributes.pop("ec", None) self.data = DictionaryOfStan() self.stack = span.stack @@ -118,7 +292,7 @@ def __init__(self, span, source, service_name, **kwargs): self.__dict__.update(kwargs) - def _populate_extra_span_attributes(self, span): + def _populate_extra_span_attributes(self, span) -> None: if span.context.trace_parent: self.tp = span.context.trace_parent if span.context.instana_ancestor: @@ -130,60 +304,70 @@ def _populate_extra_span_attributes(self, span): if span.context.correlation_id: self.crid = span.context.correlation_id - def _validate_tags(self, tags): + def _validate_attributes(self, attributes): """ - This method will loop through a set of tags to validate each key and value. + This method will loop through a set of attributes to validate each key and value. - :param tags: dict of tags - :return: dict - a filtered set of tags + :param attributes: dict of attributes + :return: dict - a filtered set of attributes """ - filtered_tags = DictionaryOfStan() - for key in tags.keys(): - validated_key, validated_value = self._validate_tag(key, tags[key]) + filtered_attributes = DictionaryOfStan() + for key in attributes.keys(): + validated_key, validated_value = self._validate_attribute( + key, attributes[key] + ) if validated_key is not None and validated_value is not None: - filtered_tags[validated_key] = validated_value - return filtered_tags + filtered_attributes[validated_key] = validated_value + return filtered_attributes - def _validate_tag(self, key, value): + def _validate_attribute(self, key, value): """ - This method will assure that and are valid to set as a tag. + This method will assure that and are valid to set as a attribute. If fails the check, an attempt will be made to convert it into something useful. - On check failure, this method will return None values indicating that the tag is + On check failure, this method will return None values indicating that the attribute is not valid and could not be converted into something useful - :param key: The tag key - :param value: The tag value + :param key: The attribute key + :param value: The attribute value :return: Tuple (key, value) """ validated_key = None validated_value = None try: - # Tag keys must be some type of text or string type + # Attribute keys must be some type of text or string type if isinstance(key, (six.text_type, six.string_types)): validated_key = key[0:1024] # Max key length of 1024 characters - if isinstance(value, (bool, float, int, list, dict, six.text_type, six.string_types)): + if isinstance( + value, + (bool, float, int, list, dict, six.text_type, six.string_types), + ): validated_value = value else: - validated_value = self._convert_tag_value(value) + validated_value = self._convert_attribute_value(value) else: - logger.debug("(non-fatal) tag names must be strings. tag discarded for %s", type(key)) + logger.debug( + "(non-fatal) attribute names must be strings. attribute discarded for %s", + type(key), + ) except Exception: - logger.debug("instana.span._validate_tag: ", exc_info=True) + logger.debug("instana.span._validate_attribute: ", exc_info=True) return (validated_key, validated_value) - def _convert_tag_value(self, value): + def _convert_attribute_value(self, value): final_value = None try: final_value = repr(value) except Exception: - final_value = "(non-fatal) span.set_tag: values must be one of these types: bool, float, int, list, " \ - "set, str or alternatively support 'repr'. tag discarded" + final_value = ( + "(non-fatal) span.set_attribute: values must be one of these types: bool, float, int, list, " + "set, str or alternatively support 'repr'. attribute discarded" + ) logger.debug(final_value, exc_info=True) return None return final_value @@ -193,7 +377,7 @@ class SDKSpan(BaseSpan): ENTRY_KIND = ["entry", "server", "consumer"] EXIT_KIND = ["exit", "client", "producer"] - def __init__(self, span, source, service_name, **kwargs): + def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name super(SDKSpan, self).__init__(span, source, service_name, **kwargs) @@ -205,306 +389,417 @@ def __init__(self, span, source, service_name, **kwargs): if service_name is not None: self.data["service"] = service_name - self.data["sdk"]["name"] = span.operation_name + self.data["sdk"]["name"] = span.name self.data["sdk"]["type"] = span_kind[0] - self.data["sdk"]["custom"]["tags"] = self._validate_tags(span.tags) + self.data["sdk"]["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) - if span.logs is not None and len(span.logs) > 0: - logs = DictionaryOfStan() - for log in span.logs: - filtered_key_values = self._validate_tags(log.key_values) - if len(filtered_key_values.keys()) > 0: - logs[repr(log.timestamp)] = filtered_key_values - self.data["sdk"]["custom"]["logs"] = logs + if span.events is not None and len(span.events) > 0: + events = DictionaryOfStan() + for event in span.events: + filtered_attributes = self._validate_attributes(event.attributes) + if len(filtered_attributes.keys()) > 0: + events[repr(event.timestamp)] = filtered_attributes + self.data["sdk"]["custom"]["events"] = events - if "arguments" in span.tags: - self.data['sdk']['arguments'] = span.tags["arguments"] + if "arguments" in span.attributes: + self.data["sdk"]["arguments"] = span.attributes["arguments"] - if "return" in span.tags: - self.data['sdk']['return'] = span.tags["return"] + if "return" in span.attributes: + self.data["sdk"]["return"] = span.attributes["return"] - if len(span.context.baggage) > 0: - self.data["baggage"] = span.context.baggage + # if len(span.context.baggage) > 0: + # self.data["baggage"] = span.context.baggage - def get_span_kind(self, span): + def get_span_kind(self, span) -> Tuple[str, int]: """ - Will retrieve the `span.kind` tag and return a tuple containing the appropriate string and integer + Will retrieve the `span.kind` attribute and return a tuple containing the appropriate string and integer values for the Instana backend - :param span: The span to search for the `span.kind` tag + :param span: The span to search for the `span.kind` attribute :return: Tuple (String, Int) """ kind = ("intermediate", 3) - if "span.kind" in span.tags: - if span.tags["span.kind"] in self.ENTRY_KIND: + if "span.kind" in span.attributes: + if span.attributes["span.kind"] in self.ENTRY_KIND: kind = ("entry", 1) - elif span.tags["span.kind"] in self.EXIT_KIND: + elif span.attributes["span.kind"] in self.EXIT_KIND: kind = ("exit", 2) return kind class RegisteredSpan(BaseSpan): - HTTP_SPANS = ("aiohttp-client", "aiohttp-server", "django", "http", "tornado-client", - "tornado-server", "urllib3", "wsgi", "asgi") - - EXIT_SPANS = ("aiohttp-client", "boto3", "cassandra", "celery-client", "couchbase", "log", "memcache", - "mongo", "mysql", "postgres", "rabbitmq", "redis", "rpc-client", "sqlalchemy", - "tornado-client", "urllib3", "pymongo", "gcs", "gcps-producer") - - ENTRY_SPANS = ("aiohttp-server", "aws.lambda.entry", "celery-worker", "django", "wsgi", "rabbitmq", - "rpc-server", "tornado-server", "gcps-consumer", "asgi") - - LOCAL_SPANS = ("render") - - def __init__(self, span, source, service_name, **kwargs): + HTTP_SPANS = ( + "aiohttp-client", + "aiohttp-server", + "django", + "http", + "tornado-client", + "tornado-server", + "urllib3", + "wsgi", + "asgi", + ) + + EXIT_SPANS = ( + "aiohttp-client", + "boto3", + "cassandra", + "celery-client", + "couchbase", + "log", + "memcache", + "mongo", + "mysql", + "postgres", + "rabbitmq", + "redis", + "rpc-client", + "sqlalchemy", + "tornado-client", + "urllib3", + "pymongo", + "gcs", + "gcps-producer", + ) + + ENTRY_SPANS = ( + "aiohttp-server", + "aws.lambda.entry", + "celery-worker", + "django", + "wsgi", + "rabbitmq", + "rpc-server", + "tornado-server", + "gcps-consumer", + "asgi", + ) + + LOCAL_SPANS = "render" + + def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name super(RegisteredSpan, self).__init__(span, source, service_name, **kwargs) - self.n = span.operation_name + self.n = span.name self.k = 1 self.data["service"] = service_name - if span.operation_name in self.ENTRY_SPANS: + if span.name in self.ENTRY_SPANS: # entry self._populate_entry_span_data(span) self._populate_extra_span_attributes(span) - elif span.operation_name in self.EXIT_SPANS: + elif span.name in self.EXIT_SPANS: self.k = 2 # exit self._populate_exit_span_data(span) - elif span.operation_name in self.LOCAL_SPANS: + elif span.name in self.LOCAL_SPANS: self.k = 3 # intermediate span self._populate_local_span_data(span) if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": self.k = 2 # exit - # unify the span operation_name for gcps-producer and gcps-consumer - if "gcps" in span.operation_name: - self.n = 'gcps' + # unify the span name for gcps-producer and gcps-consumer + if "gcps" in span.name: + self.n = "gcps" - # Store any leftover tags in the custom section - if len(span.tags) > 0: - self.data["custom"]["tags"] = self._validate_tags(span.tags) + # Store any leftover attributes in the custom section + if len(span.attributes) > 0: + self.data["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) - def _populate_entry_span_data(self, span): - if span.operation_name in self.HTTP_SPANS: - self._collect_http_tags(span) + def _populate_entry_span_data(self, span) -> None: + if span.name in self.HTTP_SPANS: + self._collect_http_attributes(span) - elif span.operation_name == "aws.lambda.entry": - self.data["lambda"]["arn"] = span.tags.pop('lambda.arn', "Unknown") + elif span.name == "aws.lambda.entry": + self.data["lambda"]["arn"] = span.attributes.pop("lambda.arn", "Unknown") self.data["lambda"]["alias"] = None self.data["lambda"]["runtime"] = "python" - self.data["lambda"]["functionName"] = span.tags.pop('lambda.name', "Unknown") - self.data["lambda"]["functionVersion"] = span.tags.pop('lambda.version', "Unknown") - self.data["lambda"]["trigger"] = span.tags.pop('lambda.trigger', None) - self.data["lambda"]["error"] = span.tags.pop('lambda.error', None) + self.data["lambda"]["functionName"] = span.attributes.pop( + "lambda.name", "Unknown" + ) + self.data["lambda"]["functionVersion"] = span.attributes.pop( + "lambda.version", "Unknown" + ) + self.data["lambda"]["trigger"] = span.attributes.pop("lambda.trigger", None) + self.data["lambda"]["error"] = span.attributes.pop("lambda.error", None) trigger_type = self.data["lambda"]["trigger"] if trigger_type in ["aws:api.gateway", "aws:application.load.balancer"]: - self._collect_http_tags(span) - elif trigger_type == 'aws:cloudwatch.events': - self.data["lambda"]["cw"]["events"]["id"] = span.tags.pop('data.lambda.cw.events.id', None) - self.data["lambda"]["cw"]["events"]["more"] = span.tags.pop('lambda.cw.events.more', False) - self.data["lambda"]["cw"]["events"]["resources"] = span.tags.pop('lambda.cw.events.resources', None) - - elif trigger_type == 'aws:cloudwatch.logs': - self.data["lambda"]["cw"]["logs"]["group"] = span.tags.pop('lambda.cw.logs.group', None) - self.data["lambda"]["cw"]["logs"]["stream"] = span.tags.pop('lambda.cw.logs.stream', None) - self.data["lambda"]["cw"]["logs"]["more"] = span.tags.pop('lambda.cw.logs.more', None) - self.data["lambda"]["cw"]["logs"]["events"] = span.tags.pop('lambda.cw.logs.events', None) - - elif trigger_type == 'aws:s3': - self.data["lambda"]["s3"]["events"] = span.tags.pop('lambda.s3.events', None) - elif trigger_type == 'aws:sqs': - self.data["lambda"]["sqs"]["messages"] = span.tags.pop('lambda.sqs.messages', None) - - elif span.operation_name == "celery-worker": - self.data["celery"]["task"] = span.tags.pop('task', None) - self.data["celery"]["task_id"] = span.tags.pop('task_id', None) - self.data["celery"]["scheme"] = span.tags.pop('scheme', None) - self.data["celery"]["host"] = span.tags.pop('host', None) - self.data["celery"]["port"] = span.tags.pop('port', None) - self.data["celery"]["retry-reason"] = span.tags.pop('retry-reason', None) - self.data["celery"]["error"] = span.tags.pop('error', None) - - elif span.operation_name == "gcps-consumer": - self.data["gcps"]["op"] = span.tags.pop('gcps.op', None) - self.data["gcps"]["projid"] = span.tags.pop('gcps.projid', None) - self.data["gcps"]["sub"] = span.tags.pop('gcps.sub', None) - - elif span.operation_name == "rabbitmq": - self.data["rabbitmq"]["exchange"] = span.tags.pop('exchange', None) - self.data["rabbitmq"]["queue"] = span.tags.pop('queue', None) - self.data["rabbitmq"]["sort"] = span.tags.pop('sort', None) - self.data["rabbitmq"]["address"] = span.tags.pop('address', None) - self.data["rabbitmq"]["key"] = span.tags.pop('key', None) - - elif span.operation_name == "rpc-server": - self.data["rpc"]["flavor"] = span.tags.pop('rpc.flavor', None) - self.data["rpc"]["host"] = span.tags.pop('rpc.host', None) - self.data["rpc"]["port"] = span.tags.pop('rpc.port', None) - self.data["rpc"]["call"] = span.tags.pop('rpc.call', None) - self.data["rpc"]["call_type"] = span.tags.pop('rpc.call_type', None) - self.data["rpc"]["params"] = span.tags.pop('rpc.params', None) - self.data["rpc"]["baggage"] = span.tags.pop('rpc.baggage', None) - self.data["rpc"]["error"] = span.tags.pop('rpc.error', None) + self._collect_http_attributes(span) + elif trigger_type == "aws:cloudwatch.events": + self.data["lambda"]["cw"]["events"]["id"] = span.attributes.pop( + "data.lambda.cw.events.id", None + ) + self.data["lambda"]["cw"]["events"]["more"] = span.attributes.pop( + "lambda.cw.events.more", False + ) + self.data["lambda"]["cw"]["events"]["resources"] = span.attributes.pop( + "lambda.cw.events.resources", None + ) + + elif trigger_type == "aws:cloudwatch.logs": + self.data["lambda"]["cw"]["logs"]["group"] = span.attributes.pop( + "lambda.cw.logs.group", None + ) + self.data["lambda"]["cw"]["logs"]["stream"] = span.attributes.pop( + "lambda.cw.logs.stream", None + ) + self.data["lambda"]["cw"]["logs"]["more"] = span.attributes.pop( + "lambda.cw.logs.more", None + ) + self.data["lambda"]["cw"]["logs"]["events"] = span.attributes.pop( + "lambda.cw.logs.events", None + ) + + elif trigger_type == "aws:s3": + self.data["lambda"]["s3"]["events"] = span.attributes.pop( + "lambda.s3.events", None + ) + elif trigger_type == "aws:sqs": + self.data["lambda"]["sqs"]["messages"] = span.attributes.pop( + "lambda.sqs.messages", None + ) + + elif span.name == "celery-worker": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["retry-reason"] = span.attributes.pop( + "retry-reason", None + ) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcps-consumer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["sub"] = span.attributes.pop("gcps.sub", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "rpc-server": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) else: - logger.debug("SpanRecorder: Unknown entry span: %s" % span.operation_name) - - def _populate_local_span_data(self, span): - if span.operation_name == "render": - self.data["render"]["name"] = span.tags.pop('name', None) - self.data["render"]["type"] = span.tags.pop('type', None) - self.data["log"]["message"] = span.tags.pop('message', None) - self.data["log"]["parameters"] = span.tags.pop('parameters', None) + logger.debug("SpanRecorder: Unknown entry span: %s" % span.name) + + def _populate_local_span_data(self, span) -> None: + if span.name == "render": + self.data["render"]["name"] = span.attributes.pop("name", None) + self.data["render"]["type"] = span.attributes.pop("type", None) + self.data["event"]["message"] = span.attributes.pop("message", None) + self.data["event"]["parameters"] = span.attributes.pop("parameters", None) else: - logger.debug("SpanRecorder: Unknown local span: %s" % span.operation_name) + logger.debug("SpanRecorder: Unknown local span: %s" % span.name) - def _populate_exit_span_data(self, span): - if span.operation_name in self.HTTP_SPANS: - self._collect_http_tags(span) + def _populate_exit_span_data(self, span) -> None: + if span.name in self.HTTP_SPANS: + self._collect_http_attributes(span) - elif span.operation_name == "boto3": - # boto3 also sends http tags - self._collect_http_tags(span) + elif span.name == "boto3": + # boto3 also sends http attributes + self._collect_http_attributes(span) - for tag in ['op', 'ep', 'reg', 'payload', 'error']: - value = span.tags.pop(tag, None) + for attribute in ["op", "ep", "reg", "payload", "error"]: + value = span.attributes.pop(attribute, None) if value is not None: - if tag == 'payload': - self.data["boto3"][tag] = self._validate_tags(value) + if attribute == "payload": + self.data["boto3"][attribute] = self._validate_attributes(value) else: - self.data["boto3"][tag] = value - - elif span.operation_name == "cassandra": - self.data["cassandra"]["cluster"] = span.tags.pop('cassandra.cluster', None) - self.data["cassandra"]["query"] = span.tags.pop('cassandra.query', None) - self.data["cassandra"]["keyspace"] = span.tags.pop('cassandra.keyspace', None) - self.data["cassandra"]["fetchSize"] = span.tags.pop('cassandra.fetchSize', None) - self.data["cassandra"]["achievedConsistency"] = span.tags.pop('cassandra.achievedConsistency', None) - self.data["cassandra"]["triedHosts"] = span.tags.pop('cassandra.triedHosts', None) - self.data["cassandra"]["fullyFetched"] = span.tags.pop('cassandra.fullyFetched', None) - self.data["cassandra"]["error"] = span.tags.pop('cassandra.error', None) - - elif span.operation_name == "celery-client": - self.data["celery"]["task"] = span.tags.pop('task', None) - self.data["celery"]["task_id"] = span.tags.pop('task_id', None) - self.data["celery"]["scheme"] = span.tags.pop('scheme', None) - self.data["celery"]["host"] = span.tags.pop('host', None) - self.data["celery"]["port"] = span.tags.pop('port', None) - self.data["celery"]["error"] = span.tags.pop('error', None) - - elif span.operation_name == "couchbase": - self.data["couchbase"]["hostname"] = span.tags.pop('couchbase.hostname', None) - self.data["couchbase"]["bucket"] = span.tags.pop('couchbase.bucket', None) - self.data["couchbase"]["type"] = span.tags.pop('couchbase.type', None) - self.data["couchbase"]["error"] = span.tags.pop('couchbase.error', None) - self.data["couchbase"]["error_type"] = span.tags.pop('couchbase.error_type', None) - self.data["couchbase"]["sql"] = span.tags.pop('couchbase.sql', None) - - elif span.operation_name == "rabbitmq": - self.data["rabbitmq"]["exchange"] = span.tags.pop('exchange', None) - self.data["rabbitmq"]["queue"] = span.tags.pop('queue', None) - self.data["rabbitmq"]["sort"] = span.tags.pop('sort', None) - self.data["rabbitmq"]["address"] = span.tags.pop('address', None) - self.data["rabbitmq"]["key"] = span.tags.pop('key', None) - - elif span.operation_name == "redis": - self.data["redis"]["connection"] = span.tags.pop('connection', None) - self.data["redis"]["driver"] = span.tags.pop('driver', None) - self.data["redis"]["command"] = span.tags.pop('command', None) - self.data["redis"]["error"] = span.tags.pop('redis.error', None) - self.data["redis"]["subCommands"] = span.tags.pop('subCommands', None) - - elif span.operation_name == "rpc-client": - self.data["rpc"]["flavor"] = span.tags.pop('rpc.flavor', None) - self.data["rpc"]["host"] = span.tags.pop('rpc.host', None) - self.data["rpc"]["port"] = span.tags.pop('rpc.port', None) - self.data["rpc"]["call"] = span.tags.pop('rpc.call', None) - self.data["rpc"]["call_type"] = span.tags.pop('rpc.call_type', None) - self.data["rpc"]["params"] = span.tags.pop('rpc.params', None) - self.data["rpc"]["baggage"] = span.tags.pop('rpc.baggage', None) - self.data["rpc"]["error"] = span.tags.pop('rpc.error', None) - - elif span.operation_name == "sqlalchemy": - self.data["sqlalchemy"]["sql"] = span.tags.pop('sqlalchemy.sql', None) - self.data["sqlalchemy"]["eng"] = span.tags.pop('sqlalchemy.eng', None) - self.data["sqlalchemy"]["url"] = span.tags.pop('sqlalchemy.url', None) - self.data["sqlalchemy"]["err"] = span.tags.pop('sqlalchemy.err', None) - - elif span.operation_name == "mysql": - self.data["mysql"]["host"] = span.tags.pop('host', None) - self.data["mysql"]["port"] = span.tags.pop('port', None) - self.data["mysql"]["db"] = span.tags.pop(ot_tags.DATABASE_INSTANCE, None) - self.data["mysql"]["user"] = span.tags.pop(ot_tags.DATABASE_USER, None) - self.data["mysql"]["stmt"] = span.tags.pop(ot_tags.DATABASE_STATEMENT, None) - self.data["mysql"]["error"] = span.tags.pop('mysql.error', None) - - elif span.operation_name == "postgres": - self.data["pg"]["host"] = span.tags.pop('host', None) - self.data["pg"]["port"] = span.tags.pop('port', None) - self.data["pg"]["db"] = span.tags.pop(ot_tags.DATABASE_INSTANCE, None) - self.data["pg"]["user"] = span.tags.pop(ot_tags.DATABASE_USER, None) - self.data["pg"]["stmt"] = span.tags.pop(ot_tags.DATABASE_STATEMENT, None) - self.data["pg"]["error"] = span.tags.pop('pg.error', None) - - elif span.operation_name == "mongo": - service = "%s:%s" % (span.tags.pop('host', None), span.tags.pop('port', None)) - namespace = "%s.%s" % (span.tags.pop('db', "?"), span.tags.pop('collection', "?")) + self.data["boto3"][attribute] = value + + elif span.name == "cassandra": + self.data["cassandra"]["cluster"] = span.attributes.pop( + "cassandra.cluster", None + ) + self.data["cassandra"]["query"] = span.attributes.pop( + "cassandra.query", None + ) + self.data["cassandra"]["keyspace"] = span.attributes.pop( + "cassandra.keyspace", None + ) + self.data["cassandra"]["fetchSize"] = span.attributes.pop( + "cassandra.fetchSize", None + ) + self.data["cassandra"]["achievedConsistency"] = span.attributes.pop( + "cassandra.achievedConsistency", None + ) + self.data["cassandra"]["triedHosts"] = span.attributes.pop( + "cassandra.triedHosts", None + ) + self.data["cassandra"]["fullyFetched"] = span.attributes.pop( + "cassandra.fullyFetched", None + ) + self.data["cassandra"]["error"] = span.attributes.pop( + "cassandra.error", None + ) + + elif span.name == "celery-client": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "couchbase": + self.data["couchbase"]["hostname"] = span.attributes.pop( + "couchbase.hostname", None + ) + self.data["couchbase"]["bucket"] = span.attributes.pop( + "couchbase.bucket", None + ) + self.data["couchbase"]["type"] = span.attributes.pop("couchbase.type", None) + self.data["couchbase"]["error"] = span.attributes.pop( + "couchbase.error", None + ) + self.data["couchbase"]["error_type"] = span.attributes.pop( + "couchbase.error_type", None + ) + self.data["couchbase"]["sql"] = span.attributes.pop("couchbase.sql", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "redis": + self.data["redis"]["connection"] = span.attributes.pop("connection", None) + self.data["redis"]["driver"] = span.attributes.pop("driver", None) + self.data["redis"]["command"] = span.attributes.pop("command", None) + self.data["redis"]["error"] = span.attributes.pop("redis.error", None) + self.data["redis"]["subCommands"] = span.attributes.pop("subCommands", None) + + elif span.name == "rpc-client": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + + elif span.name == "sqlalchemy": + self.data["sqlalchemy"]["sql"] = span.attributes.pop("sqlalchemy.sql", None) + self.data["sqlalchemy"]["eng"] = span.attributes.pop("sqlalchemy.eng", None) + self.data["sqlalchemy"]["url"] = span.attributes.pop("sqlalchemy.url", None) + self.data["sqlalchemy"]["err"] = span.attributes.pop("sqlalchemy.err", None) + + elif span.name == "mysql": + self.data["mysql"]["host"] = span.attributes.pop("host", None) + self.data["mysql"]["port"] = span.attributes.pop("port", None) + self.data["mysql"]["db"] = span.attributes.pop("db.instance", None) + self.data["mysql"]["user"] = span.attributes.pop("db.user", None) + self.data["mysql"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) + + elif span.name == "postgres": + self.data["pg"]["host"] = span.attributes.pop("host", None) + self.data["pg"]["port"] = span.attributes.pop("port", None) + self.data["pg"]["db"] = span.attributes.pop("db.instance", None) + self.data["pg"]["user"] = span.attributes.pop("db.user", None) + self.data["pg"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["pg"]["error"] = span.attributes.pop("pg.error", None) + + elif span.name == "mongo": + service = "%s:%s" % ( + span.attributes.pop("host", None), + span.attributes.pop("port", None), + ) + namespace = "%s.%s" % ( + span.attributes.pop("db", "?"), + span.attributes.pop("collection", "?"), + ) self.data["mongo"]["service"] = service self.data["mongo"]["namespace"] = namespace - self.data["mongo"]["command"] = span.tags.pop('command', None) - self.data["mongo"]["filter"] = span.tags.pop('filter', None) - self.data["mongo"]["json"] = span.tags.pop('json', None) - self.data["mongo"]["error"] = span.tags.pop('error', None) - - elif span.operation_name == "gcs": - self.data["gcs"]["op"] = span.tags.pop('gcs.op') - self.data["gcs"]["bucket"] = span.tags.pop('gcs.bucket', None) - self.data["gcs"]["object"] = span.tags.pop('gcs.object', None) - self.data["gcs"]["entity"] = span.tags.pop('gcs.entity', None) - self.data["gcs"]["range"] = span.tags.pop('gcs.range', None) - self.data["gcs"]["sourceBucket"] = span.tags.pop('gcs.sourceBucket', None) - self.data["gcs"]["sourceObject"] = span.tags.pop('gcs.sourceObject', None) - self.data["gcs"]["sourceObjects"] = span.tags.pop('gcs.sourceObjects', None) - self.data["gcs"]["destinationBucket"] = span.tags.pop('gcs.destinationBucket', None) - self.data["gcs"]["destinationObject"] = span.tags.pop('gcs.destinationObject', None) - self.data["gcs"]["numberOfOperations"] = span.tags.pop('gcs.numberOfOperations', None) - self.data["gcs"]["projectId"] = span.tags.pop('gcs.projectId', None) - self.data["gcs"]["accessId"] = span.tags.pop('gcs.accessId', None) - - elif span.operation_name == "gcps-producer": - self.data["gcps"]["op"] = span.tags.pop('gcps.op', None) - self.data["gcps"]["projid"] = span.tags.pop('gcps.projid', None) - self.data["gcps"]["top"] = span.tags.pop('gcps.top', None) - - elif span.operation_name == "log": + self.data["mongo"]["command"] = span.attributes.pop("command", None) + self.data["mongo"]["filter"] = span.attributes.pop("filter", None) + self.data["mongo"]["json"] = span.attributes.pop("json", None) + self.data["mongo"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcs": + self.data["gcs"]["op"] = span.attributes.pop("gcs.op", None) + self.data["gcs"]["bucket"] = span.attributes.pop("gcs.bucket", None) + self.data["gcs"]["object"] = span.attributes.pop("gcs.object", None) + self.data["gcs"]["entity"] = span.attributes.pop("gcs.entity", None) + self.data["gcs"]["range"] = span.attributes.pop("gcs.range", None) + self.data["gcs"]["sourceBucket"] = span.attributes.pop( + "gcs.sourceBucket", None + ) + self.data["gcs"]["sourceObject"] = span.attributes.pop( + "gcs.sourceObject", None + ) + self.data["gcs"]["sourceObjects"] = span.attributes.pop( + "gcs.sourceObjects", None + ) + self.data["gcs"]["destinationBucket"] = span.attributes.pop( + "gcs.destinationBucket", None + ) + self.data["gcs"]["destinationObject"] = span.attributes.pop( + "gcs.destinationObject", None + ) + self.data["gcs"]["numberOfOperations"] = span.attributes.pop( + "gcs.numberOfOperations", None + ) + self.data["gcs"]["projectId"] = span.attributes.pop("gcs.projectId", None) + self.data["gcs"]["accessId"] = span.attributes.pop("gcs.accessId", None) + + elif span.name == "gcps-producer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["top"] = span.attributes.pop("gcps.top", None) + + elif span.name == "log": # use last special key values - for l in span.logs: - if "message" in l.key_values: - self.data["log"]["message"] = l.key_values.pop("message", None) - if "parameters" in l.key_values: - self.data["log"]["parameters"] = l.key_values.pop("parameters", None) + for event in span.events: + if "message" in event.attributes: + self.data["event"]["message"] = event.attributes.pop( + "message", None + ) + if "parameters" in event.attributes: + self.data["event"]["parameters"] = event.attributes.pop( + "parameters", None + ) else: - logger.debug("SpanRecorder: Unknown exit span: %s" % span.operation_name) - - def _collect_http_tags(self, span): - self.data["http"]["host"] = span.tags.pop("http.host", None) - self.data["http"]["url"] = span.tags.pop(ot_tags.HTTP_URL, None) - self.data["http"]["path"] = span.tags.pop("http.path", None) - self.data["http"]["params"] = span.tags.pop('http.params', None) - self.data["http"]["method"] = span.tags.pop(ot_tags.HTTP_METHOD, None) - self.data["http"]["status"] = span.tags.pop(ot_tags.HTTP_STATUS_CODE, None) - self.data["http"]["path_tpl"] = span.tags.pop("http.path_tpl", None) - self.data["http"]["error"] = span.tags.pop('http.error', None) - - if len(span.tags) > 0: + logger.debug("SpanRecorder: Unknown exit span: %s" % span.name) + + def _collect_http_attributes(self, span) -> None: + self.data["http"]["host"] = span.attributes.pop("http.host", None) + self.data["http"]["url"] = span.attributes.pop("http.url", None) + self.data["http"]["path"] = span.attributes.pop("http.path", None) + self.data["http"]["params"] = span.attributes.pop("http.params", None) + self.data["http"]["method"] = span.attributes.pop("http.method", None) + self.data["http"]["status"] = span.attributes.pop("http.status_code", None) + self.data["http"]["path_tpl"] = span.attributes.pop("http.path_tpl", None) + self.data["http"]["error"] = span.attributes.pop("http.error", None) + + if len(span.attributes) > 0: custom_headers = [] - for key in span.tags: + for key in span.attributes: if key[0:12] == "http.header.": custom_headers.append(key) for key in custom_headers: trimmed_key = key[12:] - self.data["http"]["header"][trimmed_key] = span.tags.pop(key) + self.data["http"]["header"][trimmed_key] = span.attributes.pop(key) diff --git a/src/instana/span_context.py b/src/instana/span_context.py index 1c874a35..6c84875e 100644 --- a/src/instana/span_context.py +++ b/src/instana/span_context.py @@ -1,24 +1,25 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 +from opentelemetry.trace import SpanContext as OtelSpanContext -class SpanContext(): +class SpanContext(OtelSpanContext): def __init__( - self, - trace_id=None, - span_id=None, - baggage=None, - sampled=True, - level=1, - synthetic=False - ): + self, + trace_id=None, + span_id=None, + # baggage=None, + sampled=True, + level=1, + synthetic=False, + ) -> None: self.level = level self.trace_id = trace_id self.span_id = span_id self.sampled = sampled self.synthetic = synthetic - self._baggage = baggage or {} + # self._baggage = baggage or {} self.trace_parent = None # true/false flag self.instana_ancestor = None @@ -84,20 +85,21 @@ def correlation_id(self): def correlation_id(self, value): self._correlation_id = value - @property - def baggage(self): - return self._baggage + # @property + # def baggage(self): + # return self._baggage @property def suppression(self): return self.level == 0 - def with_baggage_item(self, key, value): - new_baggage = self._baggage.copy() - new_baggage[key] = value - return SpanContext( - trace_id=self.trace_id, - span_id=self.span_id, - sampled=self.sampled, - level=self.level, - baggage=new_baggage) + # def with_baggage_item(self, key, value): + # new_baggage = self._baggage.copy() + # new_baggage[key] = value + # return SpanContext( + # trace_id=self.trace_id, + # span_id=self.span_id, + # sampled=self.sampled, + # level=self.level, + # baggage=new_baggage, + # ) From c9371488d5dcc5f70e80f4d320c68cb4429d9c2a Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Sun, 17 Mar 2024 14:53:32 +0100 Subject: [PATCH 005/172] feat(OTel): Remove dependency of basictracer. Signed-off-by: Paulo Vital --- src/instana/recorder.py | 7 ++++--- tests/platforms/test_host_collector.py | 6 ++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/instana/recorder.py b/src/instana/recorder.py index b5875805..cf1c72af 100644 --- a/src/instana/recorder.py +++ b/src/instana/recorder.py @@ -7,8 +7,6 @@ import queue import sys -from basictracer import Sampler - from .span import RegisteredSpan, SDKSpan @@ -90,7 +88,10 @@ def record_span(self, span): self.agent.collector.span_queue.put(json_span) -class InstanaSampler(Sampler): +class InstanaSampler(object): + def __init__(self) -> None: + pass + def sampled(self, _): # We never sample return False diff --git a/tests/platforms/test_host_collector.py b/tests/platforms/test_host_collector.py index a53c801e..7ab6f064 100644 --- a/tests/platforms/test_host_collector.py +++ b/tests/platforms/test_host_collector.py @@ -177,8 +177,6 @@ def test_prepare_payload_with_snapshot_with_python_packages(self, mock_should_se self.assertEqual(snapshot['versions']['instana'], VERSION) self.assertIn('wrapt', snapshot['versions']) self.assertIn('fysom', snapshot['versions']) - self.assertIn('opentracing', snapshot['versions']) - self.assertIn('basictracer', snapshot['versions']) @patch.object(HostCollector, "should_send_snapshot_data") def test_prepare_payload_with_snapshot_disabled_python_packages(self, mock_should_send_snapshot_data): @@ -213,7 +211,7 @@ def test_prepare_payload_with_autowrapt(self, mock_should_send_snapshot_data): self.assertEqual('Autowrapt', snapshot['m']) self.assertIn('version', snapshot) self.assertGreater(len(snapshot['versions']), 5) - expected_packages = ('instana', 'wrapt', 'fysom', 'opentracing', 'basictracer') + expected_packages = ('instana', 'wrapt', 'fysom') for package in expected_packages: self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") self.assertEqual(snapshot['versions']['instana'], VERSION) @@ -236,7 +234,7 @@ def test_prepare_payload_with_autotrace(self, mock_should_send_snapshot_data): self.assertEqual('AutoTrace', snapshot['m']) self.assertIn('version', snapshot) self.assertGreater(len(snapshot['versions']), 5) - expected_packages = ('instana', 'wrapt', 'fysom', 'opentracing', 'basictracer') + expected_packages = ('instana', 'wrapt', 'fysom') for package in expected_packages: self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") self.assertEqual(snapshot['versions']['instana'], VERSION) From 193a2dc38afcab2b882eaca7b659bbc029c34d3c Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Sun, 17 Mar 2024 14:56:27 +0100 Subject: [PATCH 006/172] style: Format recorder.py and test_host_collector.py, and fix lint violations. Used ruff (vscode) to: - Black-compatible code formatting. - fix all auto-fixable violations, like unused imports. - isort-compatible import sorting. Signed-off-by: Paulo Vital --- src/instana/recorder.py | 54 +++-- tests/platforms/test_host_collector.py | 309 ++++++++++++++----------- 2 files changed, 214 insertions(+), 149 deletions(-) diff --git a/src/instana/recorder.py b/src/instana/recorder.py index cf1c72af..63ebaacd 100644 --- a/src/instana/recorder.py +++ b/src/instana/recorder.py @@ -5,46 +5,70 @@ import os import queue -import sys from .span import RegisteredSpan, SDKSpan - class StanRecorder(object): THREAD_NAME = "Instana Span Reporting" - REGISTERED_SPANS = ("aiohttp-client", "aiohttp-server", "aws.lambda.entry", - "boto3", "cassandra", "celery-client", "celery-worker", - "couchbase", "django", "gcs", "gcps-producer", - "gcps-consumer", "log", "memcache", "mongo", "mysql", - "postgres", "pymongo", "rabbitmq", "redis","render", - "rpc-client", "rpc-server", "sqlalchemy", "tornado-client", - "tornado-server", "urllib3", "wsgi", "asgi") + REGISTERED_SPANS = ( + "aiohttp-client", + "aiohttp-server", + "aws.lambda.entry", + "boto3", + "cassandra", + "celery-client", + "celery-worker", + "couchbase", + "django", + "gcs", + "gcps-producer", + "gcps-consumer", + "log", + "memcache", + "mongo", + "mysql", + "postgres", + "pymongo", + "rabbitmq", + "redis", + "render", + "rpc-client", + "rpc-server", + "sqlalchemy", + "tornado-client", + "tornado-server", + "urllib3", + "wsgi", + "asgi", + ) # Recorder thread for collection/reporting of spans thread = None - def __init__(self, agent = None): + def __init__(self, agent=None): if agent is None: # Late import to avoid circular import # pylint: disable=import-outside-toplevel from .singletons import get_agent + self.agent = get_agent() else: self.agent = agent def queue_size(self): - """ Return the size of the queue; how may spans are queued, """ + """Return the size of the queue; how may spans are queued,""" return self.agent.collector.span_queue.qsize() def queued_spans(self): - """ Get all of the spans in the queue """ + """Get all of the spans in the queue""" span = None spans = [] import time from .singletons import env_is_test + if env_is_test is True: time.sleep(1) @@ -61,8 +85,8 @@ def queued_spans(self): return spans def clear_spans(self): - """ Clear the queue of spans """ - if self.agent.collector.span_queue.empty() == False: + """Clear the queue of spans""" + if not self.agent.collector.span_queue.empty(): self.queued_spans() def record_span(self, span): @@ -91,7 +115,7 @@ def record_span(self, span): class InstanaSampler(object): def __init__(self) -> None: pass - + def sampled(self, _): # We never sample return False diff --git a/tests/platforms/test_host_collector.py b/tests/platforms/test_host_collector.py index 7ab6f064..667e7afd 100644 --- a/tests/platforms/test_host_collector.py +++ b/tests/platforms/test_host_collector.py @@ -10,13 +10,16 @@ from instana.tracer import InstanaTracer from instana.recorder import StanRecorder from instana.agent.host import HostAgent -from instana.collector.helpers.runtime import PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR +from instana.collector.helpers.runtime import ( + PATH_OF_DEPRECATED_INSTALLATION_VIA_HOST_AGENT, +) from instana.collector.host import HostCollector from instana.singletons import get_agent, set_agent, get_tracer, set_tracer from instana.version import VERSION + class TestHostCollector(unittest.TestCase): - def __init__(self, methodName='runTest'): + def __init__(self, methodName="runTest"): super(TestHostCollector, self).__init__(methodName) self.agent = None self.span_recorder = None @@ -29,14 +32,18 @@ def setUp(self): self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + '3.8.0' def tearDown(self): - """ Reset all environment variables of consequence """ + """Reset all environment variables of consequence""" variable_names = ( - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_AGENT_KEY", "INSTANA_ZONE", - "INSTANA_TAGS", "INSTANA_DISABLE_METRICS_COLLECTION", - "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", - "AUTOWRAPT_BOOTSTRAP" - ) + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_AGENT_KEY", + "INSTANA_ZONE", + "INSTANA_TAGS", + "INSTANA_DISABLE_METRICS_COLLECTION", + "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", + "AUTOWRAPT_BOOTSTRAP", + ) for variable_name in variable_names: if variable_name in os.environ: @@ -61,78 +68,102 @@ def test_prepare_payload_basics(self): self.assertTrue(payload) self.assertEqual(len(payload.keys()), 3) - self.assertIn('spans', payload) - self.assertIsInstance(payload['spans'], list) - self.assertEqual(len(payload['spans']), 0) - self.assertIn('metrics', payload) - self.assertEqual(len(payload['metrics'].keys()), 1) - self.assertIn('plugins', payload['metrics']) - self.assertIsInstance(payload['metrics']['plugins'], list) - self.assertEqual(len(payload['metrics']['plugins']), 1) - - python_plugin = payload['metrics']['plugins'][0] - self.assertEqual(python_plugin['name'], 'com.instana.plugin.python') - self.assertEqual(python_plugin['entityId'], str(os.getpid())) - self.assertIn('data', python_plugin) - self.assertIn('snapshot', python_plugin['data']) - self.assertIn('m', python_plugin['data']['snapshot']) - self.assertEqual('Manual', python_plugin['data']['snapshot']['m']) - self.assertIn('metrics', python_plugin['data']) + self.assertIn("spans", payload) + self.assertIsInstance(payload["spans"], list) + self.assertEqual(len(payload["spans"]), 0) + self.assertIn("metrics", payload) + self.assertEqual(len(payload["metrics"].keys()), 1) + self.assertIn("plugins", payload["metrics"]) + self.assertIsInstance(payload["metrics"]["plugins"], list) + self.assertEqual(len(payload["metrics"]["plugins"]), 1) + + python_plugin = payload["metrics"]["plugins"][0] + self.assertEqual(python_plugin["name"], "com.instana.plugin.python") + self.assertEqual(python_plugin["entityId"], str(os.getpid())) + self.assertIn("data", python_plugin) + self.assertIn("snapshot", python_plugin["data"]) + self.assertIn("m", python_plugin["data"]["snapshot"]) + self.assertEqual("Manual", python_plugin["data"]["snapshot"]["m"]) + self.assertIn("metrics", python_plugin["data"]) # Validate that all metrics are reported on the first run - self.assertIn('ru_utime', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_utime']), [float, int]) - self.assertIn('ru_stime', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_stime']), [float, int]) - self.assertIn('ru_maxrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_maxrss']), [float, int]) - self.assertIn('ru_ixrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_ixrss']), [float, int]) - self.assertIn('ru_idrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_idrss']), [float, int]) - self.assertIn('ru_isrss', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_isrss']), [float, int]) - self.assertIn('ru_minflt', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_minflt']), [float, int]) - self.assertIn('ru_majflt', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_majflt']), [float, int]) - self.assertIn('ru_nswap', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nswap']), [float, int]) - self.assertIn('ru_inblock', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_inblock']), [float, int]) - self.assertIn('ru_oublock', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_oublock']), [float, int]) - self.assertIn('ru_msgsnd', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_msgsnd']), [float, int]) - self.assertIn('ru_msgrcv', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_msgrcv']), [float, int]) - self.assertIn('ru_nsignals', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nsignals']), [float, int]) - self.assertIn('ru_nvcsw', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nvcsw']), [float, int]) - self.assertIn('ru_nivcsw', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['ru_nivcsw']), [float, int]) - self.assertIn('alive_threads', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['alive_threads']), [float, int]) - self.assertIn('dummy_threads', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['dummy_threads']), [float, int]) - self.assertIn('daemon_threads', python_plugin['data']['metrics']) - self.assertIn(type(python_plugin['data']['metrics']['daemon_threads']), [float, int]) - - self.assertIn('gc', python_plugin['data']['metrics']) - self.assertIsInstance(python_plugin['data']['metrics']['gc'], dict) - self.assertIn('collect0', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['collect0']), [float, int]) - self.assertIn('collect1', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['collect1']), [float, int]) - self.assertIn('collect2', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['collect2']), [float, int]) - self.assertIn('threshold0', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['threshold0']), [float, int]) - self.assertIn('threshold1', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['threshold1']), [float, int]) - self.assertIn('threshold2', python_plugin['data']['metrics']['gc']) - self.assertIn(type(python_plugin['data']['metrics']['gc']['threshold2']), [float, int]) + self.assertIn("ru_utime", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_utime"]), [float, int]) + self.assertIn("ru_stime", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_stime"]), [float, int]) + self.assertIn("ru_maxrss", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_maxrss"]), [float, int]) + self.assertIn("ru_ixrss", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_ixrss"]), [float, int]) + self.assertIn("ru_idrss", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_idrss"]), [float, int]) + self.assertIn("ru_isrss", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_isrss"]), [float, int]) + self.assertIn("ru_minflt", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_minflt"]), [float, int]) + self.assertIn("ru_majflt", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_majflt"]), [float, int]) + self.assertIn("ru_nswap", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_nswap"]), [float, int]) + self.assertIn("ru_inblock", python_plugin["data"]["metrics"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["ru_inblock"]), [float, int] + ) + self.assertIn("ru_oublock", python_plugin["data"]["metrics"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["ru_oublock"]), [float, int] + ) + self.assertIn("ru_msgsnd", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_msgsnd"]), [float, int]) + self.assertIn("ru_msgrcv", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_msgrcv"]), [float, int]) + self.assertIn("ru_nsignals", python_plugin["data"]["metrics"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["ru_nsignals"]), [float, int] + ) + self.assertIn("ru_nvcsw", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_nvcsw"]), [float, int]) + self.assertIn("ru_nivcsw", python_plugin["data"]["metrics"]) + self.assertIn(type(python_plugin["data"]["metrics"]["ru_nivcsw"]), [float, int]) + self.assertIn("alive_threads", python_plugin["data"]["metrics"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["alive_threads"]), [float, int] + ) + self.assertIn("dummy_threads", python_plugin["data"]["metrics"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["dummy_threads"]), [float, int] + ) + self.assertIn("daemon_threads", python_plugin["data"]["metrics"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["daemon_threads"]), [float, int] + ) + + self.assertIn("gc", python_plugin["data"]["metrics"]) + self.assertIsInstance(python_plugin["data"]["metrics"]["gc"], dict) + self.assertIn("collect0", python_plugin["data"]["metrics"]["gc"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["gc"]["collect0"]), [float, int] + ) + self.assertIn("collect1", python_plugin["data"]["metrics"]["gc"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["gc"]["collect1"]), [float, int] + ) + self.assertIn("collect2", python_plugin["data"]["metrics"]["gc"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["gc"]["collect2"]), [float, int] + ) + self.assertIn("threshold0", python_plugin["data"]["metrics"]["gc"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["gc"]["threshold0"]), [float, int] + ) + self.assertIn("threshold1", python_plugin["data"]["metrics"]["gc"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["gc"]["threshold1"]), [float, int] + ) + self.assertIn("threshold2", python_plugin["data"]["metrics"]["gc"]) + self.assertIn( + type(python_plugin["data"]["metrics"]["gc"]["threshold2"]), [float, int] + ) def test_prepare_payload_basics_disable_runtime_metrics(self): os.environ["INSTANA_DISABLE_METRICS_COLLECTION"] = "TRUE" @@ -142,59 +173,62 @@ def test_prepare_payload_basics_disable_runtime_metrics(self): self.assertTrue(payload) self.assertEqual(len(payload.keys()), 3) - self.assertIn('spans', payload) - self.assertIsInstance(payload['spans'], list) - self.assertEqual(len(payload['spans']), 0) - self.assertIn('metrics', payload) - self.assertEqual(len(payload['metrics'].keys()), 1) - self.assertIn('plugins', payload['metrics']) - self.assertIsInstance(payload['metrics']['plugins'], list) - self.assertEqual(len(payload['metrics']['plugins']), 1) - - python_plugin = payload['metrics']['plugins'][0] - self.assertEqual(python_plugin['name'], 'com.instana.plugin.python') - self.assertEqual(python_plugin['entityId'], str(os.getpid())) - self.assertIn('data', python_plugin) - self.assertIn('snapshot', python_plugin['data']) - self.assertIn('m', python_plugin['data']['snapshot']) - self.assertEqual('Manual', python_plugin['data']['snapshot']['m']) - self.assertNotIn('metrics', python_plugin['data']) + self.assertIn("spans", payload) + self.assertIsInstance(payload["spans"], list) + self.assertEqual(len(payload["spans"]), 0) + self.assertIn("metrics", payload) + self.assertEqual(len(payload["metrics"].keys()), 1) + self.assertIn("plugins", payload["metrics"]) + self.assertIsInstance(payload["metrics"]["plugins"], list) + self.assertEqual(len(payload["metrics"]["plugins"]), 1) + + python_plugin = payload["metrics"]["plugins"][0] + self.assertEqual(python_plugin["name"], "com.instana.plugin.python") + self.assertEqual(python_plugin["entityId"], str(os.getpid())) + self.assertIn("data", python_plugin) + self.assertIn("snapshot", python_plugin["data"]) + self.assertIn("m", python_plugin["data"]["snapshot"]) + self.assertEqual("Manual", python_plugin["data"]["snapshot"]["m"]) + self.assertNotIn("metrics", python_plugin["data"]) @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_with_python_packages(self, mock_should_send_snapshot_data): + def test_prepare_payload_with_snapshot_with_python_packages( + self, mock_should_send_snapshot_data + ): mock_should_send_snapshot_data.return_value = True self.create_agent_and_setup_tracer() payload = self.agent.collector.prepare_payload() self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] + self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('Manual', snapshot['m']) - self.assertIn('version', snapshot) - self.assertGreater(len(snapshot['versions']), 5) - self.assertEqual(snapshot['versions']['instana'], VERSION) - self.assertIn('wrapt', snapshot['versions']) - self.assertIn('fysom', snapshot['versions']) + self.assertIn("m", snapshot) + self.assertEqual("Manual", snapshot["m"]) + self.assertIn("version", snapshot) + self.assertGreater(len(snapshot["versions"]), 5) + self.assertEqual(snapshot["versions"]["instana"], VERSION) + self.assertIn("wrapt", snapshot["versions"]) + self.assertIn("fysom", snapshot["versions"]) @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_disabled_python_packages(self, mock_should_send_snapshot_data): + def test_prepare_payload_with_snapshot_disabled_python_packages( + self, mock_should_send_snapshot_data + ): mock_should_send_snapshot_data.return_value = True os.environ["INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"] = "TRUE" self.create_agent_and_setup_tracer() payload = self.agent.collector.prepare_payload() self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] + self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('Manual', snapshot['m']) - self.assertIn('version', snapshot) - self.assertEqual(len(snapshot['versions']), 1) - self.assertEqual(snapshot['versions']['instana'], VERSION) - + self.assertIn("m", snapshot) + self.assertEqual("Manual", snapshot["m"]) + self.assertIn("version", snapshot) + self.assertEqual(len(snapshot["versions"]), 1) + self.assertEqual(snapshot["versions"]["instana"], VERSION) @patch.object(HostCollector, "should_send_snapshot_data") def test_prepare_payload_with_autowrapt(self, mock_should_send_snapshot_data): @@ -204,18 +238,21 @@ def test_prepare_payload_with_autowrapt(self, mock_should_send_snapshot_data): payload = self.agent.collector.prepare_payload() self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] + self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('Autowrapt', snapshot['m']) - self.assertIn('version', snapshot) - self.assertGreater(len(snapshot['versions']), 5) - expected_packages = ('instana', 'wrapt', 'fysom') + self.assertIn("m", snapshot) + self.assertEqual("Autowrapt", snapshot["m"]) + self.assertIn("version", snapshot) + self.assertGreater(len(snapshot["versions"]), 5) + expected_packages = ("instana", "wrapt", "fysom") for package in expected_packages: - self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") - self.assertEqual(snapshot['versions']['instana'], VERSION) - + self.assertIn( + package, + snapshot["versions"], + f"{package} not found in snapshot['versions']", + ) + self.assertEqual(snapshot["versions"]["instana"], VERSION) @patch.object(HostCollector, "should_send_snapshot_data") def test_prepare_payload_with_autotrace(self, mock_should_send_snapshot_data): @@ -227,14 +264,18 @@ def test_prepare_payload_with_autotrace(self, mock_should_send_snapshot_data): payload = self.agent.collector.prepare_payload() self.assertTrue(payload) - self.assertIn('snapshot', payload['metrics']['plugins'][0]['data']) - snapshot = payload['metrics']['plugins'][0]['data']['snapshot'] + self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] self.assertTrue(snapshot) - self.assertIn('m', snapshot) - self.assertEqual('AutoTrace', snapshot['m']) - self.assertIn('version', snapshot) - self.assertGreater(len(snapshot['versions']), 5) - expected_packages = ('instana', 'wrapt', 'fysom') + self.assertIn("m", snapshot) + self.assertEqual("AutoTrace", snapshot["m"]) + self.assertIn("version", snapshot) + self.assertGreater(len(snapshot["versions"]), 5) + expected_packages = ("instana", "wrapt", "fysom") for package in expected_packages: - self.assertIn(package, snapshot['versions'], f"{package} not found in snapshot['versions']") - self.assertEqual(snapshot['versions']['instana'], VERSION) + self.assertIn( + package, + snapshot["versions"], + f"{package} not found in snapshot['versions']", + ) + self.assertEqual(snapshot["versions"]["instana"], VERSION) From 0f5d0faa20f3190b6e27e7e531c559634e56c302 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 28 Mar 2024 05:55:54 +0100 Subject: [PATCH 007/172] feat(OTel): Add Sampler abstract class and adapt InstanaSampler class. Signed-off-by: Paulo Vital --- src/instana/recorder.py | 9 --------- src/instana/sampling.py | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 src/instana/sampling.py diff --git a/src/instana/recorder.py b/src/instana/recorder.py index 63ebaacd..74926705 100644 --- a/src/instana/recorder.py +++ b/src/instana/recorder.py @@ -110,12 +110,3 @@ def record_span(self, span): # logger.debug("Recorded span: %s", json_span) self.agent.collector.span_queue.put(json_span) - - -class InstanaSampler(object): - def __init__(self) -> None: - pass - - def sampled(self, _): - # We never sample - return False diff --git a/src/instana/sampling.py b/src/instana/sampling.py new file mode 100644 index 00000000..7f84f786 --- /dev/null +++ b/src/instana/sampling.py @@ -0,0 +1,43 @@ +# (c) Copyright IBM Corp. 2024 + +import abc +import enum + + +class SamplingPolicy(enum.Enum): + # IsRecording() == False + # Span will not be recorded and all events and attributes will be dropped. + # https://opentelemetry.io/docs/specs/otel/trace/api/#isrecording + DROP = 0 + # IsRecording() == True, but Sampled flag MUST NOT be set. + RECORD_ONLY = 1 + # IsRecording() == True AND Sampled flag MUST be set. + RECORD_AND_SAMPLE = 2 + + +class Sampler(abc.ABC): + """Samplers choose whether the span is recorded or dropped. + + A variety of sampling algorithms are available, and choosing which sampler + to use and how to configure it is one of the most confusing parts of + setting up a tracing system. + """ + + @abc.abstractmethod + def sampled(self) -> bool: + """ + Returns if a span was dropped (False) or recorded (True). + + Calling a span “sampled” can mean it was “sampled out” (dropped) + or “sampled in” (recorded). + """ + pass + + +class InstanaSampler(Sampler): + def __init__(self) -> None: + # Instana never samples. + self._sampled: SamplingPolicy = SamplingPolicy.DROP + + def sampled(self) -> bool: + return False if self._sampled == SamplingPolicy.DROP else True From c3877cf658ea0b3c758634446f37368c5bf6ac86 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 28 Mar 2024 11:49:37 +0100 Subject: [PATCH 008/172] refactor(tracer): Tracer migration to use OTel. Co-authored-by: Varsha GS Signed-off-by: Paulo Vital --- src/instana/propagators/exceptions.py | 11 + src/instana/propagators/format.py | 52 +++++ src/instana/singletons.py | 35 +-- src/instana/tracer.py | 301 +++++++++++++++----------- tests/helpers.py | 2 +- 5 files changed, 255 insertions(+), 146 deletions(-) create mode 100644 src/instana/propagators/exceptions.py create mode 100644 src/instana/propagators/format.py diff --git a/src/instana/propagators/exceptions.py b/src/instana/propagators/exceptions.py new file mode 100644 index 00000000..7e613c5f --- /dev/null +++ b/src/instana/propagators/exceptions.py @@ -0,0 +1,11 @@ +# (c) Copyright IBM Corp. 2024 + + +class UnsupportedFormatException(Exception): + """UnsupportedFormatException should be used when the provided format + value is unknown or disallowed by the :class:`InstanaTracer`. + + See :meth:`InstanaTracer.inject()` and :meth:`InstanaTracer.extract()`. + """ + + pass diff --git a/src/instana/propagators/format.py b/src/instana/propagators/format.py new file mode 100644 index 00000000..9049c4e1 --- /dev/null +++ b/src/instana/propagators/format.py @@ -0,0 +1,52 @@ +# (c) Copyright IBM Corp. 2024 + + +class Format(object): + """A namespace for builtin carrier formats. + + These static constants are intended for use in the :meth:`Tracer.inject()` + and :meth:`Tracer.extract()` methods. E.g.:: + + tracer.inject(span.context, Format.BINARY, binary_carrier) + + """ + + BINARY = "binary" + """ + The BINARY format represents SpanContexts in an opaque bytearray carrier. + + For both :meth:`Tracer.inject()` and :meth:`Tracer.extract()` the carrier + should be a bytearray instance. :meth:`Tracer.inject()` must append to the + bytearray carrier (rather than replace its contents). + """ + + TEXT_MAP = "text_map" + """ + The TEXT_MAP format represents :class:`SpanContext`\\ s in a python + ``dict`` mapping from strings to strings. + + Both the keys and the values have unrestricted character sets (unlike the + HTTP_HEADERS format). + + NOTE: The TEXT_MAP carrier ``dict`` may contain unrelated data (e.g., + arbitrary gRPC metadata). As such, the :class:`Tracer` implementation + should use a prefix or other convention to distinguish tracer-specific + key:value pairs. + """ + + HTTP_HEADERS = "http_headers" + """ + The HTTP_HEADERS format represents :class:`SpanContext`\\ s in a python + ``dict`` mapping from character-restricted strings to strings. + + Keys and values in the HTTP_HEADERS carrier must be suitable for use as + HTTP headers (without modification or further escaping). That is, the + keys have a greatly restricted character set, casing for the keys may not + be preserved by various intermediaries, and the values should be + URL-escaped. + + NOTE: The HTTP_HEADERS carrier ``dict`` may contain unrelated data (e.g., + arbitrary gRPC metadata). As such, the :class:`Tracer` implementation + should use a prefix or other convention to distinguish tracer-specific + key:value pairs. + """ diff --git a/src/instana/singletons.py b/src/instana/singletons.py index c93416ca..7b2db17d 100644 --- a/src/instana/singletons.py +++ b/src/instana/singletons.py @@ -3,11 +3,11 @@ import os -import opentracing +from opentelemetry import trace from .autoprofile.profiler import Profiler from .log import logger -from .tracer import InstanaTracer +from .tracer import InstanaTracerProvider agent = None tracer = None @@ -96,34 +96,23 @@ def set_agent(new_agent): agent = new_agent -# The global OpenTracing compatible tracer used internally by +# The global OpenTelemetry compatible tracer used internally by # this package. -tracer = InstanaTracer(recorder=span_recorder) +provider = InstanaTracerProvider(recorder=span_recorder) +provider.add_span_processor(agent) -try: - from opentracing.scope_managers.contextvars import ContextVarsScopeManager +# Sets the global default tracer provider +trace.set_tracer_provider(provider) - async_tracer = InstanaTracer( - scope_manager=ContextVarsScopeManager(), recorder=span_recorder - ) -except Exception: - logger.debug("Error setting up async_tracer:", exc_info=True) - -# Mock the tornado tracer until tornado is detected and instrumented first -tornado_tracer = tracer +# Creates a tracer from the global tracer provider +tracer = trace.get_tracer("instana.tracer") +async_tracer = trace.get_tracer("instana.async.tracer") +tornado_tracer = None def setup_tornado_tracer(): global tornado_tracer - from opentracing.scope_managers.tornado import TornadoScopeManager - - tornado_tracer = InstanaTracer( - scope_manager=TornadoScopeManager(), recorder=span_recorder - ) - - -# Set ourselves as the tracer. -opentracing.tracer = tracer + tornado_tracer = trace.get_tracer("instana.tornado.tracer") def get_tracer(): diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 2d1d2def..3ad74d8b 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -6,135 +6,162 @@ import re import time import traceback - -import opentracing as ot -from basictracer import BasicTracer - -from .util.ids import generate_id -from .span_context import SpanContext -from .span import InstanaSpan, RegisteredSpan -from .recorder import StanRecorder, InstanaSampler -from .propagators.http_propagator import HTTPPropagator -from .propagators.text_propagator import TextPropagator -from .propagators.binary_propagator import BinaryPropagator - - -class InstanaTracer(BasicTracer): - def __init__(self, scope_manager=None, recorder=None): - - if recorder is None: - recorder = StanRecorder() - - super(InstanaTracer, self).__init__( - recorder, InstanaSampler(), scope_manager) - - self._propagators[ot.Format.HTTP_HEADERS] = HTTPPropagator() - self._propagators[ot.Format.TEXT_MAP] = TextPropagator() - self._propagators[ot.Format.BINARY] = BinaryPropagator() - - def start_active_span(self, - operation_name, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False, - finish_on_close=True): - - # create a new Span - span = self.start_span( - operation_name=operation_name, - child_of=child_of, - references=references, - tags=tags, - start_time=start_time, - ignore_active_span=ignore_active_span, +from typing import Iterator, Mapping, Optional, Union + +from opentelemetry.context.context import Context +from opentelemetry.trace import ( + SpanKind, + Tracer, + TracerProvider, + _Links, + get_current_span, + use_span, +) +from opentelemetry.util import types + +from instana.agent.host import HostAgent +from instana.agent.test import TestAgent +from instana.log import logger +from instana.propagators.binary_propagator import BinaryPropagator +from instana.propagators.format import Format +from instana.propagators.http_propagator import HTTPPropagator +from instana.propagators.text_propagator import TextPropagator +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler, Sampler +from instana.span import InstanaSpan, RegisteredSpan +from instana.span_context import SpanContext +from instana.util.ids import generate_id + + +class InstanaTracerProvider(TracerProvider): + def __init__( + self, + sampler: Optional[Sampler] = None, + recorder: Optional[StanRecorder] = None, + span_processor: Optional[Union[HostAgent, TestAgent]] = None, + ) -> None: + self._span_processor = ( + span_processor or HostAgent() ) - return self.scope_manager.activate(span, finish_on_close) - - def start_span(self, - operation_name=None, - child_of=None, - references=None, - tags=None, - start_time=None, - ignore_active_span=False): - "Taken from BasicTracer so we can override generate_id calls to ours" - - start_time = time.time() if start_time is None else start_time - - # See if we have a parent_ctx in `references` - parent_ctx = None - if child_of is not None: - parent_ctx = ( - child_of if isinstance(child_of, SpanContext) - else child_of.context) - elif references is not None and len(references) > 0: - # TODO only the first reference is currently used - parent_ctx = references[0].referenced_context - - # retrieve the active SpanContext - if not ignore_active_span and parent_ctx is None: - scope = self.scope_manager.active - if scope is not None: - parent_ctx = scope.span.context - - # Assemble the child ctx - gid = generate_id() - ctx = SpanContext(span_id=gid) - if parent_ctx is not None and parent_ctx.trace_id is not None: - if hasattr(parent_ctx, '_baggage') and parent_ctx._baggage is not None: - ctx._baggage = parent_ctx._baggage.copy() - ctx.trace_id = parent_ctx.trace_id - ctx.sampled = parent_ctx.sampled - ctx.long_trace_id = parent_ctx.long_trace_id - ctx.trace_parent = parent_ctx.trace_parent - ctx.instana_ancestor = parent_ctx.instana_ancestor - ctx.level = parent_ctx.level - ctx.correlation_type = parent_ctx.correlation_type - ctx.correlation_id = parent_ctx.correlation_id - ctx.traceparent = parent_ctx.traceparent - ctx.tracestate = parent_ctx.tracestate - else: - ctx.trace_id = gid - ctx.sampled = self.sampler.sampled(ctx.trace_id) - if parent_ctx is not None: - ctx.level = parent_ctx.level - ctx.correlation_type = parent_ctx.correlation_type - ctx.correlation_id = parent_ctx.correlation_id - ctx.traceparent = parent_ctx.traceparent - ctx.tracestate = parent_ctx.tracestate - - # Tie it all together - span = InstanaSpan(self, - operation_name=operation_name, - context=ctx, - parent_id=(None if parent_ctx is None else parent_ctx.span_id), - tags=tags, - 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) - - return span + self.sampler = InstanaSampler() if sampler is None else sampler + self.recorder = StanRecorder() if recorder is None else recorder + self._propagators = {} + self._propagators[Format.HTTP_HEADERS] = HTTPPropagator() + self._propagators[Format.TEXT_MAP] = TextPropagator() + self._propagators[Format.BINARY] = BinaryPropagator() + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: Optional[str] = None, + schema_url: Optional[str] = None, + ) -> Tracer: + if not instrumenting_module_name: # Reject empty strings too. + instrumenting_module_name = "" + logger.error("get_tracer called with missing module name.") + + return InstanaTracer( + self.sampler, + self.recorder, + self._span_processor, + self._propagators, + ) - def inject(self, span_context, format, carrier, disable_w3c_trace_context=False): - if format in self._propagators: - return self._propagators[format].inject(span_context, carrier, disable_w3c_trace_context) +class InstanaTracer(Tracer): + """Handles :class:`InstanaSpan` creation and in-process context propagation. + + This class provides methods for manipulating the context, creating spans, + and controlling spans' lifecycles. + """ + def __init__( + self, + sampler: Optional[Sampler] = None, + recorder: Optional[StanRecorder] = None, + span_processor: Optional[Union[HostAgent, TestAgent]] = None, + propagators: Optional[Mapping[str, Union[BinaryPropagator, HTTPPropagator, TextPropagator]]] = None, + ) -> None: + self._tracer_id = generate_id() + self._sampler = sampler + self._recorder = recorder + self._span_processor = span_processor + self._propagators = propagators + + @property + def tracer_id(self) -> str: + return self._tracer_id + + @property + def recorder(self) -> Optional[StanRecorder]: + return self._recorder + + def start_span( + self, + name: str, + context: Optional[Context] = None, + kind: SpanKind = SpanKind.INTERNAL, + attributes: types.Attributes = None, + links: _Links = None, + start_time: Optional[int] = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + ) -> InstanaSpan: + + parent_context = get_current_span(context).get_span_context() + if parent_context is not None and not isinstance(parent_context, SpanContext): + raise TypeError( + "parent_context must be a SpanContext or None." + ) + + span_context = self._create_span_context(parent_context) + span = InstanaSpan( + name, + span_context, + parent_id=(None if parent_context is None else parent_context.span_id), + start_time=(time.time_ns() if start_time is None else start_time), + attributes=attributes, + # events: Sequence[Event] = None, + ) - raise ot.UnsupportedFormatException() + if parent_context is not None: + span.synthetic = parent_context.synthetic - def extract(self, format, carrier, disable_w3c_trace_context=False): - if format in self._propagators: - return self._propagators[format].extract(carrier, disable_w3c_trace_context) + if name in RegisteredSpan.EXIT_SPANS: + self._add_stack(span) - raise ot.UnsupportedFormatException() + return span - def __add_stack(self, span, limit=30): + def start_as_current_span( + self, + name: str, + context: Optional[Context] = None, + kind: SpanKind = SpanKind.INTERNAL, + attributes: types.Attributes = None, + links: _Links = None, + start_time: Optional[int] = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + end_on_exit: bool = True, + ) -> Iterator[InstanaSpan]: + span = self.start_span( + name=name, + context=context, + kind=kind, + attributes=attributes, + links=links, + start_time=start_time, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, + ) + with use_span( + span, + end_on_exit=end_on_exit, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, + ) as span: + yield span + + def _add_stack(self, span: InstanaSpan, limit: Optional[int] = 30) -> None: """ Adds a backtrace to . The default length limit for stack traces is 30 frames. A hard limit of 40 frames is enforced. @@ -171,6 +198,36 @@ def __add_stack(self, span, limit=30): # No fail pass + def _create_span_context(self, parent_context: SpanContext) -> SpanContext: + """Creates a new SpanContext based on the given parent context.""" + + if parent_context is not None and parent_context.trace_id is not None: + trace_id = parent_context.trace_id + span_id = generate_id() + sampled = parent_context.sampled + else: + trace_id = self.tracer_id + span_id = self.tracer_id + sampled = self._tracer_provider.sampler.sampled() + + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + sampled=sampled, + level=(parent_context.level if parent_context is not None else 1), + synthetic=False + ) + + if parent_context is not None: + span_context.long_trace_id = parent_context.long_trace_id + span_context.trace_parent = parent_context.trace_parent + span_context.instana_ancestor = parent_context.instana_ancestor + span_context.correlation_type = parent_context.correlation_type + span_context.correlation_id = parent_context.correlation_id + span_context.traceparent = parent_context.traceparent + span_context.tracestate = parent_context.tracestate + + return span_context # Used by __add_stack re_tracer_frame = re.compile(r"/instana/.*\.py$") diff --git a/tests/helpers.py b/tests/helpers.py index 95fe3e61..35727523 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -146,7 +146,7 @@ def launch_traced_request(url): logger.warn("Launching request with a root SDK span name of 'launch_traced_request'") - with tracer.start_active_span('launch_traced_request'): + with tracer.start_as_current_span('launch_traced_request'): response = requests.get(url) return response From 808ef70f9b8caa7f520806a8bcf3726b807c1b99 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 28 Mar 2024 11:54:04 +0100 Subject: [PATCH 009/172] style: format singletons.py, tracer.py and tests/helpers.py Used ruff (vscode) to: - Black-compatible code formatting. - fix all auto-fixable violations, like unused imports. - isort-compatible import sorting. Signed-off-by: Paulo Vital --- src/instana/singletons.py | 1 - src/instana/tracer.py | 54 +++++++++++++++++------------------- tests/helpers.py | 58 ++++++++++++++++++++------------------- 3 files changed, 55 insertions(+), 58 deletions(-) diff --git a/src/instana/singletons.py b/src/instana/singletons.py index 7b2db17d..de605439 100644 --- a/src/instana/singletons.py +++ b/src/instana/singletons.py @@ -6,7 +6,6 @@ from opentelemetry import trace from .autoprofile.profiler import Profiler -from .log import logger from .tracer import InstanaTracerProvider agent = None diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 3ad74d8b..9095e694 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -40,9 +40,7 @@ def __init__( recorder: Optional[StanRecorder] = None, span_processor: Optional[Union[HostAgent, TestAgent]] = None, ) -> None: - self._span_processor = ( - span_processor or HostAgent() - ) + self._span_processor = span_processor or HostAgent() self.sampler = InstanaSampler() if sampler is None else sampler self.recorder = StanRecorder() if recorder is None else recorder @@ -50,7 +48,7 @@ def __init__( self._propagators[Format.HTTP_HEADERS] = HTTPPropagator() self._propagators[Format.TEXT_MAP] = TextPropagator() self._propagators[Format.BINARY] = BinaryPropagator() - + def get_tracer( self, instrumenting_module_name: str, @@ -68,18 +66,22 @@ def get_tracer( self._propagators, ) + class InstanaTracer(Tracer): """Handles :class:`InstanaSpan` creation and in-process context propagation. This class provides methods for manipulating the context, creating spans, and controlling spans' lifecycles. """ + def __init__( self, sampler: Optional[Sampler] = None, recorder: Optional[StanRecorder] = None, span_processor: Optional[Union[HostAgent, TestAgent]] = None, - propagators: Optional[Mapping[str, Union[BinaryPropagator, HTTPPropagator, TextPropagator]]] = None, + propagators: Optional[ + Mapping[str, Union[BinaryPropagator, HTTPPropagator, TextPropagator]] + ] = None, ) -> None: self._tracer_id = generate_id() self._sampler = sampler @@ -106,12 +108,9 @@ def start_span( record_exception: bool = True, set_status_on_exception: bool = True, ) -> InstanaSpan: - parent_context = get_current_span(context).get_span_context() if parent_context is not None and not isinstance(parent_context, SpanContext): - raise TypeError( - "parent_context must be a SpanContext or None." - ) + raise TypeError("parent_context must be a SpanContext or None.") span_context = self._create_span_context(parent_context) span = InstanaSpan( @@ -132,18 +131,18 @@ def start_span( return span def start_as_current_span( - self, - name: str, - context: Optional[Context] = None, - kind: SpanKind = SpanKind.INTERNAL, - attributes: types.Attributes = None, - links: _Links = None, - start_time: Optional[int] = None, - record_exception: bool = True, - set_status_on_exception: bool = True, - end_on_exit: bool = True, - ) -> Iterator[InstanaSpan]: - span = self.start_span( + self, + name: str, + context: Optional[Context] = None, + kind: SpanKind = SpanKind.INTERNAL, + attributes: types.Attributes = None, + links: _Links = None, + start_time: Optional[int] = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + end_on_exit: bool = True, + ) -> Iterator[InstanaSpan]: + span = self.start_span( name=name, context=context, kind=kind, @@ -182,16 +181,12 @@ def _add_stack(self, span: InstanaSpan, limit: Optional[int] = 30) -> None: if re_with_stan_frame.search(frame[2]) is not None: continue - sanitized_stack.append({ - "c": frame[0], - "n": frame[1], - "m": frame[2] - }) + sanitized_stack.append({"c": frame[0], "n": frame[1], "m": frame[2]}) if len(sanitized_stack) > limit: # (limit * -1) gives us negative form of used for # slicing from the end of the list. e.g. stack[-30:] - span.stack = sanitized_stack[(limit*-1):] + span.stack = sanitized_stack[(limit * -1) :] else: span.stack = sanitized_stack except Exception: @@ -215,7 +210,7 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: span_id=span_id, sampled=sampled, level=(parent_context.level if parent_context is not None else 1), - synthetic=False + synthetic=False, ) if parent_context is not None: @@ -229,6 +224,7 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: return span_context + # Used by __add_stack re_tracer_frame = re.compile(r"/instana/.*\.py$") -re_with_stan_frame = re.compile('with_instana') +re_with_stan_frame = re.compile("with_instana") diff --git a/tests/helpers.py b/tests/helpers.py index 35727523..30caf5ac 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -9,51 +9,51 @@ """ Cassandra Environment """ -testenv['cassandra_host'] = os.environ.get('CASSANDRA_HOST', '127.0.0.1') -testenv['cassandra_username'] = os.environ.get('CASSANDRA_USERNAME', 'Administrator') -testenv['cassandra_password'] = os.environ.get('CASSANDRA_PASSWORD', 'password') +testenv["cassandra_host"] = os.environ.get("CASSANDRA_HOST", "127.0.0.1") +testenv["cassandra_username"] = os.environ.get("CASSANDRA_USERNAME", "Administrator") +testenv["cassandra_password"] = os.environ.get("CASSANDRA_PASSWORD", "password") """ CouchDB Environment """ -testenv['couchdb_host'] = os.environ.get('COUCHDB_HOST', '127.0.0.1') -testenv['couchdb_username'] = os.environ.get('COUCHDB_USERNAME', 'Administrator') -testenv['couchdb_password'] = os.environ.get('COUCHDB_PASSWORD', 'password') +testenv["couchdb_host"] = os.environ.get("COUCHDB_HOST", "127.0.0.1") +testenv["couchdb_username"] = os.environ.get("COUCHDB_USERNAME", "Administrator") +testenv["couchdb_password"] = os.environ.get("COUCHDB_PASSWORD", "password") """ MySQL Environment """ -if 'MYSQL_HOST' in os.environ: - testenv['mysql_host'] = os.environ['MYSQL_HOST'] +if "MYSQL_HOST" in os.environ: + testenv["mysql_host"] = os.environ["MYSQL_HOST"] else: - testenv['mysql_host'] = '127.0.0.1' + testenv["mysql_host"] = "127.0.0.1" -testenv['mysql_port'] = int(os.environ.get('MYSQL_PORT', '3306')) -testenv['mysql_db'] = os.environ.get('MYSQL_DATABASE', 'instana_test_db') -testenv['mysql_user'] = os.environ.get('MYSQL_USER', 'root') -testenv['mysql_pw'] = os.environ.get('MYSQL_ROOT_PASSWORD', 'passw0rd') +testenv["mysql_port"] = int(os.environ.get("MYSQL_PORT", "3306")) +testenv["mysql_db"] = os.environ.get("MYSQL_DATABASE", "instana_test_db") +testenv["mysql_user"] = os.environ.get("MYSQL_USER", "root") +testenv["mysql_pw"] = os.environ.get("MYSQL_ROOT_PASSWORD", "passw0rd") """ PostgreSQL Environment """ -testenv['postgresql_host'] = os.environ.get('POSTGRES_HOST', '127.0.0.1') -testenv['postgresql_port'] = int(os.environ.get('POSTGRES_PORT', '5432')) -testenv['postgresql_db'] = os.environ.get('POSTGRES_DB', 'instana_test_db') -testenv['postgresql_user'] = os.environ.get('POSTGRES_USER', 'root') -testenv['postgresql_pw'] = os.environ.get('POSTGRES_PW', 'passw0rd') +testenv["postgresql_host"] = os.environ.get("POSTGRES_HOST", "127.0.0.1") +testenv["postgresql_port"] = int(os.environ.get("POSTGRES_PORT", "5432")) +testenv["postgresql_db"] = os.environ.get("POSTGRES_DB", "instana_test_db") +testenv["postgresql_user"] = os.environ.get("POSTGRES_USER", "root") +testenv["postgresql_pw"] = os.environ.get("POSTGRES_PW", "passw0rd") """ Redis Environment """ -testenv['redis_host'] = os.environ.get('REDIS_HOST', '127.0.0.1') +testenv["redis_host"] = os.environ.get("REDIS_HOST", "127.0.0.1") """ MongoDB Environment """ -testenv['mongodb_host'] = os.environ.get('MONGO_HOST', '127.0.0.1') -testenv['mongodb_port'] = os.environ.get('MONGO_PORT', '27017') -testenv['mongodb_user'] = os.environ.get('MONGO_USER', None) -testenv['mongodb_pw'] = os.environ.get('MONGO_PW', None) +testenv["mongodb_host"] = os.environ.get("MONGO_HOST", "127.0.0.1") +testenv["mongodb_port"] = os.environ.get("MONGO_PORT", "27017") +testenv["mongodb_user"] = os.environ.get("MONGO_USER", None) +testenv["mongodb_pw"] = os.environ.get("MONGO_PW", None) def drop_log_spans_from_list(spans): @@ -66,7 +66,7 @@ def drop_log_spans_from_list(spans): """ new_list = [] for span in spans: - if span.n != 'log': + if span.n != "log": new_list.append(span) return new_list @@ -84,8 +84,8 @@ def fail_with_message_and_span_dump(msg, spans): span_dump = "\nDumping all collected spans (%d) -->\n" % span_count if span_count > 0: for span in spans: - span.stack = '' - span_dump += repr(span) + '\n' + span.stack = "" + span_dump += repr(span) + "\n" pytest.fail(msg + span_dump, True) @@ -144,9 +144,11 @@ def launch_traced_request(url): from instana.log import logger from instana.singletons import tracer - logger.warn("Launching request with a root SDK span name of 'launch_traced_request'") + logger.warn( + "Launching request with a root SDK span name of 'launch_traced_request'" + ) - with tracer.start_as_current_span('launch_traced_request'): + with tracer.start_as_current_span("launch_traced_request"): response = requests.get(url) return response From 384be665d5ffca6aefae9b7c36720c6f29d8ecc7 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 2 Apr 2024 13:52:08 +0530 Subject: [PATCH 010/172] fix: handle circular imports caused by singletons.env_is_test Signed-off-by: Varsha GS --- src/instana/collector/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/instana/collector/base.py b/src/instana/collector/base.py index c1576688..009fe3be 100644 --- a/src/instana/collector/base.py +++ b/src/instana/collector/base.py @@ -5,16 +5,17 @@ A Collector launches a background thread and continually collects & reports data. The data can be any combination of metrics, snapshot data and spans. """ -import sys import threading +from os import environ from ..log import logger -from ..singletons import env_is_test from ..util import every, DictionaryOfStan import queue # pylint: disable=import-error +# TODO: Use mock.patch() or unittest.mock to mock the testing env +env_is_test = "INSTANA_TEST" in environ class BaseCollector(object): """ From 8a4afb01bbe3610d90d3ef0b2d01e119ad0e6c0c Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 4 Apr 2024 10:55:35 +0530 Subject: [PATCH 011/172] feat(OTel): Enhance SpanContext and tracing utilities. - contextmanager for start_as_current_span(). - use trace_flags.sampled instead of sampled. - Add Instana specific attributes to SpanContext. - Add parent Span_Context class arguments as necessary arguments to fix the serialization of binary objects. - Use format_span_id() for both representation of the trace_id and span_id since Instana uses 64-bit integers. Co-authored-by: Paulo Vital Signed-off-by: Varsha GS --- src/instana/span.py | 38 +++++++- src/instana/span_context.py | 171 +++++++++++++++++++++--------------- src/instana/tracer.py | 15 ++-- 3 files changed, 144 insertions(+), 80 deletions(-) diff --git a/src/instana/span.py b/src/instana/span.py index 3c7f820b..21d9d305 100644 --- a/src/instana/span.py +++ b/src/instana/span.py @@ -18,9 +18,19 @@ from threading import Lock from time import time_ns -from opentelemetry.trace import Span # , SpanContext +from opentelemetry.trace import ( + Span, + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + INVALID_SPAN_ID, + INVALID_TRACE_ID, + _SPAN_KEY, +) from opentelemetry.util import types from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.trace.span import NonRecordingSpan +from opentelemetry.context import get_value +from opentelemetry.context.context import Context from .span_context import SpanContext from .log import logger @@ -265,6 +275,32 @@ def assure_errored(self) -> None: logger.debug("span.assure_errored", exc_info=True) +INVALID_SPAN_CONTEXT = SpanContext( + trace_id=INVALID_TRACE_ID, + span_id=INVALID_SPAN_ID, + is_remote=False, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, +) +INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) + + +def get_current_span(context: Optional[Context] = None) -> InstanaSpan: + """Retrieve the current span. + + Args: + context: A Context object. If one is not passed, the + default current context is used instead. + + Returns: + The Span set in the context if it exists. INVALID_SPAN otherwise. + """ + span = get_value(_SPAN_KEY, context=context) + if span is None or not isinstance(span, InstanaSpan): + return INVALID_SPAN + return span + + class BaseSpan(object): sy = None diff --git a/src/instana/span_context.py b/src/instana/span_context.py index 6c84875e..ac14e61d 100644 --- a/src/instana/span_context.py +++ b/src/instana/span_context.py @@ -1,105 +1,130 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 + +import typing + from opentelemetry.trace import SpanContext as OtelSpanContext +from opentelemetry.trace.span import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + TraceFlags, + TraceState, + format_span_id, +) + class SpanContext(OtelSpanContext): - def __init__( - self, - trace_id=None, - span_id=None, - # baggage=None, - sampled=True, + """The state of a Span to propagate between processes. + + This class includes the immutable attributes of a :class:`.Span` that must + be propagated to a span's children and across process boundaries. + + Required Args: + trace_id: The ID of the trace that this span belongs to. + span_id: This span's ID. + is_remote: True if propagated from a remote parent. + """ + + def __new__( + cls, + trace_id: int, + span_id: int, + is_remote: bool, + trace_flags: typing.Optional[TraceFlags] = DEFAULT_TRACE_OPTIONS, + trace_state: typing.Optional[TraceState] = DEFAULT_TRACE_STATE, level=1, synthetic=False, - ) -> None: - - self.level = level - self.trace_id = trace_id - self.span_id = span_id - self.sampled = sampled - self.synthetic = synthetic - # self._baggage = baggage or {} - - self.trace_parent = None # true/false flag - self.instana_ancestor = None - self.long_trace_id = None - self.correlation_type = None - self.correlation_id = None - self.traceparent = None # temporary storage of the validated traceparent header of the incoming request - self.tracestate = None # temporary storage of the tracestate header + trace_parent=None, # true/false flag, + instana_ancestor=None, + long_trace_id=None, + correlation_type=None, + correlation_id=None, + traceparent=None, # temporary storage of the validated traceparent header of the incoming request + tracestate=None, # temporary storage of the tracestate header + **kwargs, + ) -> "SpanContext": + instance = super().__new__(cls, trace_id, span_id, is_remote, trace_flags, trace_state) + return tuple.__new__( + cls, + ( + instance.trace_id, + instance.span_id, + instance.is_remote, + instance.trace_flags, + instance.trace_state, + instance.is_valid, + level, + synthetic, + trace_parent, # true/false flag, + instana_ancestor, + long_trace_id, + correlation_type, + correlation_id, + traceparent, # temporary storage of the validated traceparent header of the incoming request + tracestate, # temporary storage of the tracestate header + ), + ) + + def __getnewargs__( + self, + ): # -> typing.Tuple[int, int, bool, "TraceFlags", "TraceState", int, bool, bool]: + return ( + self.trace_id, + self.span_id, + self.is_remote, + self.trace_flags, + self.trace_state, + self.level, + self.synthetic, + self.trace_parent, + self.instana_ancestor, + self.long_trace_id, + self.correlation_type, + self.correlation_id, + self.traceparent, + self.tracestate, + ) @property - def traceparent(self): - return self._traceparent - - @traceparent.setter - def traceparent(self, value): - self._traceparent = value + def level(self) -> int: + return self[6] @property - def tracestate(self): - return self._tracestate - - @tracestate.setter - def tracestate(self, value): - self._tracestate = value + def synthetic(self) -> bool: + return self[7] @property - def trace_parent(self): - return self._trace_parent - - @trace_parent.setter - def trace_parent(self, value): - self._trace_parent = value + def trace_parent(self) -> bool: + return self[8] @property def instana_ancestor(self): - return self._instana_ancestor - - @instana_ancestor.setter - def instana_ancestor(self, value): - self._instana_ancestor = value + return self[9] @property def long_trace_id(self): - return self._long_trace_id - - @long_trace_id.setter - def long_trace_id(self, value): - self._long_trace_id = value + return self[10] @property def correlation_type(self): - return self._correlation_type - - @correlation_type.setter - def correlation_type(self, value): - self._correlation_type = value + return self[11] @property def correlation_id(self): - return self._correlation_id + return self[12] - @correlation_id.setter - def correlation_id(self, value): - self._correlation_id = value + @property + def traceparent(self): + return self[13] - # @property - # def baggage(self): - # return self._baggage + @property + def tracestate(self): + return self[14] @property - def suppression(self): + def suppression(self) -> bool: return self.level == 0 - # def with_baggage_item(self, key, value): - # new_baggage = self._baggage.copy() - # new_baggage[key] = value - # return SpanContext( - # trace_id=self.trace_id, - # span_id=self.span_id, - # sampled=self.sampled, - # level=self.level, - # baggage=new_baggage, - # ) + def __repr__(self) -> str: + return f"{type(self).__name__}(trace_id=0x{format_span_id(self.trace_id)}, span_id=0x{format_span_id(self.span_id)}, trace_flags=0x{self.trace_flags:02x}, trace_state={self.trace_state!r}, is_remote={self.is_remote}, synthetic={self.synthetic})" diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 9095e694..dcbfc4c8 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -7,6 +7,7 @@ import time import traceback from typing import Iterator, Mapping, Optional, Union +from contextlib import contextmanager from opentelemetry.context.context import Context from opentelemetry.trace import ( @@ -14,7 +15,6 @@ Tracer, TracerProvider, _Links, - get_current_span, use_span, ) from opentelemetry.util import types @@ -28,7 +28,7 @@ from instana.propagators.text_propagator import TextPropagator from instana.recorder import StanRecorder from instana.sampling import InstanaSampler, Sampler -from instana.span import InstanaSpan, RegisteredSpan +from instana.span import InstanaSpan, RegisteredSpan, get_current_span from instana.span_context import SpanContext from instana.util.ids import generate_id @@ -110,7 +110,7 @@ def start_span( ) -> InstanaSpan: parent_context = get_current_span(context).get_span_context() if parent_context is not None and not isinstance(parent_context, SpanContext): - raise TypeError("parent_context must be a SpanContext or None.") + raise TypeError("parent_context must be an Instana SpanContext or None.") span_context = self._create_span_context(parent_context) span = InstanaSpan( @@ -130,6 +130,7 @@ def start_span( return span + @contextmanager def start_as_current_span( self, name: str, @@ -199,16 +200,18 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: if parent_context is not None and parent_context.trace_id is not None: trace_id = parent_context.trace_id span_id = generate_id() - sampled = parent_context.sampled + trace_flags = parent_context.trace_flags.sampled + is_remote = parent_context.is_remote else: trace_id = self.tracer_id span_id = self.tracer_id - sampled = self._tracer_provider.sampler.sampled() + trace_flags = self._tracer_provider.sampler.sampled() span_context = SpanContext( trace_id=trace_id, span_id=span_id, - sampled=sampled, + trace_flags=trace_flags, + is_remote=is_remote, level=(parent_context.level if parent_context is not None else 1), synthetic=False, ) From 0f9cb80579e602614afeb1d17c82807e82f56505 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 8 Apr 2024 10:28:26 +0200 Subject: [PATCH 012/172] feat(OTel): Enhance Tracer classes. - Refactor InstanaTracerProvider class for better performance. - Add the missing add_span_processor() method for InstanaTracerProvider. - Refactor InstanaTracer constructor removing all Optional arguments. - Fix the start_span() method to handle the root SpanContext properly. - Use TraceFlags as trace_flags arguments in new SpanContext objects. Signed-off-by: Paulo Vital --- src/instana/tracer.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index dcbfc4c8..4d7f0440 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -6,12 +6,13 @@ import re import time import traceback -from typing import Iterator, Mapping, Optional, Union from contextlib import contextmanager +from typing import Iterator, Mapping, Optional, Union from opentelemetry.context.context import Context from opentelemetry.trace import ( SpanKind, + TraceFlags, Tracer, TracerProvider, _Links, @@ -40,10 +41,9 @@ def __init__( recorder: Optional[StanRecorder] = None, span_processor: Optional[Union[HostAgent, TestAgent]] = None, ) -> None: + self.sampler = sampler or InstanaSampler() + self.recorder = recorder or StanRecorder() self._span_processor = span_processor or HostAgent() - - self.sampler = InstanaSampler() if sampler is None else sampler - self.recorder = StanRecorder() if recorder is None else recorder self._propagators = {} self._propagators[Format.HTTP_HEADERS] = HTTPPropagator() self._propagators[Format.TEXT_MAP] = TextPropagator() @@ -66,6 +66,13 @@ def get_tracer( self._propagators, ) + def add_span_processor( + self, + span_processor: Union[HostAgent, TestAgent], + ) -> None: + """Registers a new SpanProcessor for the TracerProvider.""" + self._span_processor = span_processor + class InstanaTracer(Tracer): """Handles :class:`InstanaSpan` creation and in-process context propagation. @@ -76,12 +83,11 @@ class InstanaTracer(Tracer): def __init__( self, - sampler: Optional[Sampler] = None, - recorder: Optional[StanRecorder] = None, - span_processor: Optional[Union[HostAgent, TestAgent]] = None, - propagators: Optional[ - Mapping[str, Union[BinaryPropagator, HTTPPropagator, TextPropagator]] - ] = None, + sampler: Sampler, + recorder: StanRecorder, + span_processor: Union[HostAgent, TestAgent], + propagators: + Mapping[str, Union[BinaryPropagator, HTTPPropagator, TextPropagator]], ) -> None: self._tracer_id = generate_id() self._sampler = sampler @@ -109,9 +115,14 @@ def start_span( set_status_on_exception: bool = True, ) -> InstanaSpan: parent_context = get_current_span(context).get_span_context() + if parent_context is not None and not isinstance(parent_context, SpanContext): raise TypeError("parent_context must be an Instana SpanContext or None.") + if parent_context is not None and not parent_context.is_valid: + # We probably have a INVALID_SPAN_CONTEXT. + parent_context = None + span_context = self._create_span_context(parent_context) span = InstanaSpan( name, @@ -200,12 +211,12 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: if parent_context is not None and parent_context.trace_id is not None: trace_id = parent_context.trace_id span_id = generate_id() - trace_flags = parent_context.trace_flags.sampled + trace_flags = parent_context.trace_flags is_remote = parent_context.is_remote else: trace_id = self.tracer_id span_id = self.tracer_id - trace_flags = self._tracer_provider.sampler.sampled() + trace_flags = TraceFlags(self._sampler.sampled()) span_context = SpanContext( trace_id=trace_id, From c89fd9dff67380cc6da09d207600be4d42c90508 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 10 Apr 2024 16:04:46 +0200 Subject: [PATCH 013/172] refactor: Tracer and Span ID generation. Following the OpenTelemetry API, guarantee Traces and Spans IDs are now 64-bit integers instead of strings. The data transmited to the Instana Agents or Backend are still 16HEXDIG strings. Signed-off-by: Paulo Vital --- src/instana/collector/aws_eks_fargate.py | 3 +- src/instana/collector/aws_fargate.py | 4 ++- src/instana/collector/aws_lambda.py | 3 +- src/instana/collector/google_cloud_run.py | 3 +- src/instana/collector/host.py | 12 ++++--- src/instana/collector/utils.py | 24 +++++++++++++ src/instana/tracer.py | 1 + src/instana/util/ids.py | 42 ++++++++++------------- 8 files changed, 60 insertions(+), 32 deletions(-) create mode 100644 src/instana/collector/utils.py diff --git a/src/instana/collector/aws_eks_fargate.py b/src/instana/collector/aws_eks_fargate.py index c6a2d8f0..ea9e43cd 100644 --- a/src/instana/collector/aws_eks_fargate.py +++ b/src/instana/collector/aws_eks_fargate.py @@ -5,6 +5,7 @@ """ from time import time +from instana.collector.utils import format_trace_and_span_ids from instana.log import logger from instana.collector.base import BaseCollector from instana.collector.helpers.eks.process import EKSFargateProcessHelper @@ -35,7 +36,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_trace_and_span_ids(self.queued_spans()) with_snapshot = self.should_send_snapshot_data() diff --git a/src/instana/collector/aws_fargate.py b/src/instana/collector/aws_fargate.py index 74c54c59..2d9d6791 100644 --- a/src/instana/collector/aws_fargate.py +++ b/src/instana/collector/aws_fargate.py @@ -9,6 +9,8 @@ from time import time import requests +from instana.collector.utils import format_trace_and_span_ids + from ..log import logger from .base import BaseCollector from ..util import DictionaryOfStan, validate_url @@ -137,7 +139,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_trace_and_span_ids(self.queued_spans()) with_snapshot = self.should_send_snapshot_data() diff --git a/src/instana/collector/aws_lambda.py b/src/instana/collector/aws_lambda.py index 5964e301..5a21da74 100644 --- a/src/instana/collector/aws_lambda.py +++ b/src/instana/collector/aws_lambda.py @@ -4,6 +4,7 @@ """ AWS Lambda Collector: Manages the periodic collection of metrics & snapshot data """ +from instana.collector.utils import format_trace_and_span_ids from ..log import logger from .base import BaseCollector from ..util import DictionaryOfStan @@ -47,7 +48,7 @@ def prepare_payload(self): payload["metrics"] = None if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_trace_and_span_ids(self.queued_spans()) if self.should_send_snapshot_data(): payload["metrics"] = self.snapshot_data diff --git a/src/instana/collector/google_cloud_run.py b/src/instana/collector/google_cloud_run.py index 27f5ec29..eba5ab7f 100644 --- a/src/instana/collector/google_cloud_run.py +++ b/src/instana/collector/google_cloud_run.py @@ -8,6 +8,7 @@ from time import time import requests +from instana.collector.utils import format_trace_and_span_ids from instana.log import logger from instana.collector.base import BaseCollector from instana.util import DictionaryOfStan, validate_url @@ -102,7 +103,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_trace_and_span_ids(self.queued_spans()) self.fetching_start_time = int(time()) delta = self.fetching_start_time - self.__last_gcr_md_full_fetch diff --git a/src/instana/collector/host.py b/src/instana/collector/host.py index 2ec2bd8b..6b09a7eb 100644 --- a/src/instana/collector/host.py +++ b/src/instana/collector/host.py @@ -5,10 +5,12 @@ Host Collector: Manages the periodic collection of metrics & snapshot data """ from time import time -from ..log import logger -from .base import BaseCollector -from ..util import DictionaryOfStan -from .helpers.runtime import RuntimeHelper + +from instana.collector.base import BaseCollector +from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.utils import format_trace_and_span_ids +from instana.log import logger +from instana.util import DictionaryOfStan class HostCollector(BaseCollector): @@ -67,7 +69,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = self.queued_spans() + payload["spans"] = format_trace_and_span_ids(self.queued_spans()) if not self.profile_queue.empty(): payload["profiles"] = self.queued_profiles() diff --git a/src/instana/collector/utils.py b/src/instana/collector/utils.py new file mode 100644 index 00000000..4bb4e9ff --- /dev/null +++ b/src/instana/collector/utils.py @@ -0,0 +1,24 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import List + +from opentelemetry.trace.span import format_span_id + +from instana.span import InstanaSpan + + +def format_trace_and_span_ids( + queued_spans: List[InstanaSpan], +) -> List[InstanaSpan]: + """ + Format the Trace, Parent Span, and Span IDs of Spans to be a 64-bit + Hexadecimal String instead of Integer before being pushed to a + Collector (or Instana Agent). + """ + spans = [] + for span in queued_spans: + span.t = format_span_id(span.t) + span.p = format_span_id(span.p) + span.s = format_span_id(span.s) + spans.append(span) + return spans diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 4d7f0440..ad89cc3b 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -217,6 +217,7 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: trace_id = self.tracer_id span_id = self.tracer_id trace_flags = TraceFlags(self._sampler.sampled()) + is_remote = False span_context = SpanContext( trace_id=trace_id, diff --git a/src/instana/util/ids.py b/src/instana/util/ids.py index 3d6e8d01..81792027 100644 --- a/src/instana/util/ids.py +++ b/src/instana/util/ids.py @@ -4,30 +4,32 @@ import os import time import random +from typing import Union + +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID _rnd = random.Random() _current_pid = 0 -BAD_ID = "BADCAFFE" # Bad Caffe +def generate_id() -> int: + """Get a new ID. -def generate_id(): - """ Generate a 64bit base 16 ID for use as a Span or Trace ID """ + Returns: + A 64-bit int for use as a Span or Trace ID. + """ global _current_pid pid = os.getpid() if _current_pid != pid: _current_pid = pid _rnd.seed(int(1000000 * time.time()) ^ pid) - new_id = format(_rnd.randint(0, 18446744073709551615), '02x') - - if len(new_id) < 16: - new_id = new_id.zfill(16) + new_id = _rnd.randint(0, _SPAN_ID_MAX_VALUE) return new_id -def header_to_long_id(header): +def header_to_long_id(header: Union[bytes, str]) -> int: """ We can receive headers in the following formats: 1. unsigned base 16 hex string (or bytes) of variable length @@ -40,23 +42,19 @@ def header_to_long_id(header): header = header.decode('utf-8') if not isinstance(header, str): - return BAD_ID + return INVALID_SPAN_ID try: - # Test that header is truly a hexadecimal value before we try to convert - int(header, 16) - - length = len(header) - if length < 16: + if len(header) < 16: # Left pad ID with zeros header = header.zfill(16) - return header + return int(header, 16) except ValueError: - return BAD_ID + return INVALID_SPAN_ID -def header_to_id(header): +def header_to_id(header: Union[bytes, str]) -> int: """ We can receive headers in the following formats: 1. unsigned base 16 hex string (or bytes) of variable length @@ -69,12 +67,9 @@ def header_to_id(header): header = header.decode('utf-8') if not isinstance(header, str): - return BAD_ID + return INVALID_SPAN_ID try: - # Test that header is truly a hexadecimal value before we try to convert - int(header, 16) - length = len(header) if length < 16: # Left pad ID with zeros @@ -82,6 +77,7 @@ def header_to_id(header): elif length > 16: # Phase 0: Discard everything but the last 16byte header = header[-16:] - return header + + return int(header, 16) except ValueError: - return BAD_ID + return INVALID_SPAN_ID From 6c22d20dfdeacc8337e0dab1b3f9ce2bdc90e899 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 10 Apr 2024 16:12:53 +0200 Subject: [PATCH 014/172] test(OTel): Disable auto instrumentation. Disabled the load of the auto instrumentation for tests and the load of the instana package from the beginning of test execution. Signed-off-by: Paulo Vital --- tests/conftest.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8086edc3..04084d49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,10 +11,7 @@ # Set our testing flags os.environ["INSTANA_TEST"] = "true" -# os.environ["INSTANA_DEBUG"] = "true" - -# Make sure the instana package is fully loaded -import instana +os.environ["INSTANA_DISABLE_AUTO_INSTR"] = "true" collect_ignore_glob = [ "*autoprofile*", From ef6bd51f5918975ab268a7d9a431a79050324b1f Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 10 Apr 2024 16:19:57 +0200 Subject: [PATCH 015/172] fix(test): Skip test_stan_recorder.py on macOS. Adds a pytest.mark.skipif running on macOS to avoid the raise of a NotImplementedError when calling multiprocessing.Queue.qsize(). Signed-off-by: Paulo Vital --- tests/recorder/test_stan_recorder.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/recorder/test_stan_recorder.py b/tests/recorder/test_stan_recorder.py index adb08e78..5f9940f6 100644 --- a/tests/recorder/test_stan_recorder.py +++ b/tests/recorder/test_stan_recorder.py @@ -1,9 +1,17 @@ -from instana.recorder import StanRecorder - from multiprocessing import Queue +import sys from unittest import TestCase from unittest.mock import NonCallableMagicMock, PropertyMock +import pytest + +from instana.recorder import StanRecorder + + +@pytest.mark.skipif( + sys.platform == "darwin", + reason="Avoiding NotImplementedError when calling multiprocessing.Queue.qsize()", +) class TestStanRecorderTC(TestCase): def setUp(self): mock_agent = NonCallableMagicMock() @@ -13,7 +21,9 @@ def setUp(self): self.mock_suppressed_span = NonCallableMagicMock() self.mock_suppressed_span.context = NonCallableMagicMock() self.mock_suppressed_property = PropertyMock(return_value=True) - type(self.mock_suppressed_span.context).suppression = self.mock_suppressed_property + type( + self.mock_suppressed_span.context + ).suppression = self.mock_suppressed_property def test_record_span_with_suppression(self): # Ensure that the queue is empty From 05cdad5734ddaba9e8eee7de838ca419cf662bf2 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 10 Apr 2024 16:20:36 +0200 Subject: [PATCH 016/172] style: format conftest.py Used ruff (vscode) to: - Black-compatible code formatting. - fix all auto-fixable violations, like unused imports. - isort-compatible import sorting. Signed-off-by: Paulo Vital --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 04084d49..48a30a2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import importlib.util import os import sys + import pytest if importlib.util.find_spec("celery"): From d44d6f01ccf6e457ae07840871f4c15bc4ce9132 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 10 Apr 2024 16:21:49 +0200 Subject: [PATCH 017/172] refactor: test_id_management.py to handle int IDs. Signed-off-by: Paulo Vital --- tests/test_id_management.py | 107 +++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/tests/test_id_management.py b/tests/test_id_management.py index fb3badbb..c10d2b51 100644 --- a/tests/test_id_management.py +++ b/tests/test_id_management.py @@ -1,56 +1,63 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2017 -import unittest -import instana - - -class TestIdManagement(unittest.TestCase): - def test_id_generation(self): - count = 0 - while count <= 10000: - id = instana.util.ids.generate_id() - base10_id = int(id, 16) - self.assertGreaterEqual(base10_id, 0) - self.assertLessEqual(base10_id, 18446744073709551615) - count += 1 - - - def test_various_header_to_id_conversion(self): - # Get a hex string to test against & convert - header_id = instana.util.ids.generate_id() - converted_id = instana.util.ids.header_to_long_id(header_id) - self.assertEqual(header_id, converted_id) - - # Hex value - result should be left padded - result = instana.util.ids.header_to_long_id('abcdef') - self.assertEqual('0000000000abcdef', result) +import pytest +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID - # Hex value - result = instana.util.ids.header_to_long_id('0123456789abcdef') - self.assertEqual('0123456789abcdef', result) - - # Very long incoming header should just return the rightmost 16 bytes - result = instana.util.ids.header_to_long_id('0x0123456789abcdef0123456789abcdef') - self.assertEqual('0x0123456789abcdef0123456789abcdef', result) - - - def test_header_to_id_conversion_with_bogus_header(self): - # Bogus nil arg - bogus_result = instana.util.ids.header_to_long_id(None) - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) - - # Bogus Integer arg - bogus_result = instana.util.ids.header_to_long_id(1234) - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) - - # Bogus Array arg - bogus_result = instana.util.ids.header_to_long_id([1234]) - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) +import instana - # Bogus Hex Values in String - bogus_result = instana.util.ids.header_to_long_id('0xZZZZZZ') - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) - bogus_result = instana.util.ids.header_to_long_id('ZZZZZZ') - self.assertEqual(instana.util.ids.BAD_ID, bogus_result) +def test_id_generation(): + count = 0 + while count <= 10000: + id = instana.util.ids.generate_id() + assert id >= 0 + assert id > INVALID_SPAN_ID + assert id <= _SPAN_ID_MAX_VALUE + count += 1 + + +@pytest.mark.parametrize( + "str_id, id", + [ + ("BADCAFFE", 3135025150), + ("abcdef", 11259375), + ("0123456789abcdef", 81985529216486895), + ("0x0123456789abcdef0123456789abcdef", 1512366075204170929049582354406559215), + (None, INVALID_SPAN_ID), + (1234, INVALID_SPAN_ID), + ([1234], INVALID_SPAN_ID), + ("0xZZZZZZ", INVALID_SPAN_ID), + ("ZZZZZZ", INVALID_SPAN_ID), + (b"BADCAFFE", 3135025150), + (b"abcdef", 11259375), + (b"0123456789abcdef", 81985529216486895), + (b"0x0123456789abcdef0123456789abcdef", 1512366075204170929049582354406559215), + ], +) +def test_header_to_long_id(str_id, id): + result = instana.util.ids.header_to_long_id(str_id) + assert result == id + + +@pytest.mark.parametrize( + "str_id, id", + [ + ("BADCAFFE", 3135025150), + ("abcdef", 11259375), + ("0123456789abcdef", 81985529216486895), + ("0x0123456789abcdef0123456789abcdef", 81985529216486895), + (None, INVALID_SPAN_ID), + (1234, INVALID_SPAN_ID), + ([1234], INVALID_SPAN_ID), + ("0xZZZZZZ", INVALID_SPAN_ID), + ("ZZZZZZ", INVALID_SPAN_ID), + (b"BADCAFFE", 3135025150), + (b"abcdef", 11259375), + (b"0123456789abcdef", 81985529216486895), + (b"0x0123456789abcdef0123456789abcdef", 81985529216486895), + ], +) +def test_header_to_id(str_id, id): + result = instana.util.ids.header_to_id(str_id) + assert result == id From 339a4006208eec3f257cdb49535cc2bc124e931b Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 10 Apr 2024 16:23:46 +0200 Subject: [PATCH 018/172] test(OTel): Add TracerProvider and Tracer tests. Add new unit tests to check the implementation of the new TracerProvider and Tracer classed and their methods following OpenTelemetry API. Signed-off-by: Paulo Vital --- tests/test_tracer.py | 154 ++++++++++++++++++++++++++++++++++ tests/test_tracer_provider.py | 54 ++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 tests/test_tracer.py create mode 100644 tests/test_tracer_provider.py diff --git a/tests/test_tracer.py b/tests/test_tracer.py new file mode 100644 index 00000000..b7fb558f --- /dev/null +++ b/tests/test_tracer.py @@ -0,0 +1,154 @@ +# (c) Copyright IBM Corp. 2024 + +from unittest.mock import patch +from opentelemetry.trace import set_span_in_context +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID +import pytest + +from instana.span import InstanaSpan +from instana.span_context import SpanContext +from instana.tracer import InstanaTracer, InstanaTracerProvider + + +def test_tracer_defaults() -> None: + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + + assert tracer.tracer_id > INVALID_SPAN_ID + assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE + assert tracer.recorder == provider.recorder + assert tracer._sampler == provider.sampler + assert tracer._span_processor == provider._span_processor + assert tracer._propagators == provider._propagators + +def test_tracer_start_span(span) -> None: + span_name = "test-span" + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + parent_context = set_span_in_context(span) + span = tracer.start_span(name=span_name, context=parent_context) + + assert span + assert isinstance(span, InstanaSpan) + assert span.name == span_name + assert not span.stack + + +def test_tracer_start_span_with_stack(span: InstanaSpan) -> None: + span_name = "log" + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + span = tracer.start_span(name=span_name) + + assert span + assert isinstance(span, InstanaSpan) + assert span.name == span_name + assert span.stack + + stack_0 = span.stack[0] + assert 3 == len(stack_0) + assert "c" in stack_0.keys() + assert "n" in stack_0.keys() + assert "m" in stack_0.keys() + + +def test_tracer_start_span_Exception(mocker, span) -> None: + span_name = "test-span" + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + + parent_context = set_span_in_context(span) + + mocker.patch("instana.span.InstanaSpan.get_span_context", return_value={"key": "value"}) + with pytest.raises(TypeError): + tracer.start_span(name=span_name, context=parent_context) + + +def test_tracer_start_as_current_span() -> None: + span_name = "test-span" + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + with tracer.start_as_current_span(name=span_name) as span: + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == span_name + + +def test_tracer_create_span_context(span_context: SpanContext) -> None: + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + new_span_context = tracer._create_span_context(span_context) + + assert span_context.trace_id == new_span_context.trace_id + assert span_context.span_id != new_span_context.span_id + assert span_context.long_trace_id == new_span_context.long_trace_id + + +def test_tracer_add_stack_high_limit(span: InstanaSpan) -> None: + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + tracer._add_stack(span, 50) + + assert span.stack + assert 40 >= len(span.stack) + + stack_0 = span.stack[0] + assert 3 == len(stack_0) + assert "c" in stack_0.keys() + assert "n" in stack_0.keys() + assert "m" in stack_0.keys() + + +def test_tracer_add_stack_low_limit(span: InstanaSpan) -> None: + provider = InstanaTracerProvider() + tracer = InstanaTracer( + provider.sampler, + provider.recorder, + provider._span_processor, + provider._propagators, + ) + tracer._add_stack(span, 5) + + assert span.stack + assert 5 >= len(span.stack) + + stack_0 = span.stack[0] + assert 3 == len(stack_0) + assert "c" in stack_0.keys() + assert "n" in stack_0.keys() + assert "m" in stack_0.keys() diff --git a/tests/test_tracer_provider.py b/tests/test_tracer_provider.py new file mode 100644 index 00000000..d5222fad --- /dev/null +++ b/tests/test_tracer_provider.py @@ -0,0 +1,54 @@ +# (c) Copyright IBM Corp. 2024 + +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID +from pytest import LogCaptureFixture + +from instana.agent.host import HostAgent +from instana.agent.test import TestAgent +from instana.propagators.binary_propagator import BinaryPropagator +from instana.propagators.format import Format +from instana.propagators.http_propagator import HTTPPropagator +from instana.propagators.text_propagator import TextPropagator +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler +from instana.tracer import InstanaTracer, InstanaTracerProvider + + +def test_tracer_provider_defaults() -> None: + provider = InstanaTracerProvider() + assert isinstance(provider.sampler, InstanaSampler) + assert isinstance(provider.recorder, StanRecorder) + assert isinstance(provider._span_processor, HostAgent) + assert len(provider._propagators) == 3 + assert isinstance(provider._propagators[Format.HTTP_HEADERS], HTTPPropagator) + assert isinstance(provider._propagators[Format.TEXT_MAP], TextPropagator) + assert isinstance(provider._propagators[Format.BINARY], BinaryPropagator) + + +def test_tracer_provider_get_tracer() -> None: + provider = InstanaTracerProvider() + tracer = provider.get_tracer("instana.test.tracer") + + assert isinstance(tracer, InstanaTracer) + assert tracer.tracer_id > INVALID_SPAN_ID + assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE + + +def test_tracer_provider_get_tracer_empty_instrumenting_module_name( + caplog: LogCaptureFixture, +) -> None: + provider = InstanaTracerProvider() + tracer = provider.get_tracer("") + + assert "get_tracer called with missing module name." == caplog.record_tuples[0][2] + assert isinstance(tracer, InstanaTracer) + assert tracer.tracer_id > INVALID_SPAN_ID + assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE + + +def test_tracer_provider_add_span_processor() -> None: + provider = InstanaTracerProvider() + assert isinstance(provider._span_processor, HostAgent) + + provider.add_span_processor(TestAgent()) + assert isinstance(provider._span_processor, TestAgent) From ee5b63ee1ca959a544872874ac40d584845b3cc5 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 11 Apr 2024 16:54:48 +0200 Subject: [PATCH 019/172] style: collector files Add type hints to methods and used ruff (vscode) to: - Black-compatible code formatting. - fix all auto-fixable violations, like unused imports. - isort-compatible import sorting. Signed-off-by: Paulo Vital --- src/instana/collector/aws_eks_fargate.py | 7 +- src/instana/collector/aws_fargate.py | 46 +-- src/instana/collector/aws_lambda.py | 18 +- src/instana/collector/base.py | 36 ++- src/instana/collector/google_cloud_run.py | 57 ++-- src/instana/collector/helpers/base.py | 6 +- src/instana/collector/helpers/eks/process.py | 8 +- .../collector/helpers/fargate/container.py | 68 ++-- src/instana/collector/helpers/process.py | 18 +- src/instana/collector/helpers/runtime.py | 303 +++++++++++++----- src/instana/collector/host.py | 31 +- 11 files changed, 415 insertions(+), 183 deletions(-) diff --git a/src/instana/collector/aws_eks_fargate.py b/src/instana/collector/aws_eks_fargate.py index ea9e43cd..dac335e9 100644 --- a/src/instana/collector/aws_eks_fargate.py +++ b/src/instana/collector/aws_eks_fargate.py @@ -5,16 +5,17 @@ """ from time import time -from instana.collector.utils import format_trace_and_span_ids -from instana.log import logger + from instana.collector.base import BaseCollector from instana.collector.helpers.eks.process import EKSFargateProcessHelper from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.utils import format_trace_and_span_ids +from instana.log import logger from instana.util import DictionaryOfStan class EKSFargateCollector(BaseCollector): - """ Collector for EKS Pods on AWS Fargate """ + """Collector for EKS Pods on AWS Fargate""" def __init__(self, agent): super(EKSFargateCollector, self).__init__(agent) diff --git a/src/instana/collector/aws_fargate.py b/src/instana/collector/aws_fargate.py index 2d9d6791..d3168d32 100644 --- a/src/instana/collector/aws_fargate.py +++ b/src/instana/collector/aws_fargate.py @@ -4,27 +4,27 @@ """ AWS Fargate Collector: Manages the periodic collection of metrics & snapshot data """ -import os + import json +import os from time import time + import requests +from instana.collector.base import BaseCollector +from instana.collector.helpers.fargate.container import ContainerHelper +from instana.collector.helpers.fargate.docker import DockerHelper +from instana.collector.helpers.fargate.process import FargateProcessHelper +from instana.collector.helpers.fargate.task import TaskHelper +from instana.collector.helpers.runtime import RuntimeHelper from instana.collector.utils import format_trace_and_span_ids - -from ..log import logger -from .base import BaseCollector -from ..util import DictionaryOfStan, validate_url -from ..singletons import env_is_test - -from .helpers.fargate.process import FargateProcessHelper -from .helpers.runtime import RuntimeHelper -from .helpers.fargate.task import TaskHelper -from .helpers.fargate.docker import DockerHelper -from .helpers.fargate.container import ContainerHelper +from instana.log import logger +from instana.singletons import env_is_test +from instana.util import DictionaryOfStan, validate_url class AWSFargateCollector(BaseCollector): - """ Collector for AWS Fargate """ + """Collector for AWS Fargate""" def __init__(self, agent): super(AWSFargateCollector, self).__init__(agent) @@ -37,14 +37,16 @@ def __init__(self, agent): self.ecmu = os.environ.get("ECS_CONTAINER_METADATA_URI", "") if self.ecmu == "" or validate_url(self.ecmu) is False: - logger.warning("AWSFargateCollector: ECS_CONTAINER_METADATA_URI not in environment or invalid URL. " - "Instana will not be able to monitor this environment") + logger.warning( + "AWSFargateCollector: ECS_CONTAINER_METADATA_URI not in environment or invalid URL. " + "Instana will not be able to monitor this environment" + ) self.ready_to_start = False self.ecmu_url_root = self.ecmu - self.ecmu_url_task = self.ecmu + '/task' - self.ecmu_url_stats = self.ecmu + '/stats' - self.ecmu_url_task_stats = self.ecmu + '/task/stats' + self.ecmu_url_task = self.ecmu + "/task" + self.ecmu_url_stats = self.ecmu + "/stats" + self.ecmu_url_task_stats = self.ecmu + "/task/stats" # Timestamp in seconds of the last time we fetched all ECMU data self.last_ecmu_full_fetch = 0 @@ -86,7 +88,9 @@ def __init__(self, agent): def start(self): if self.ready_to_start is False: - logger.warning("AWS Fargate Collector is missing requirements and cannot monitor this environment.") + logger.warning( + "AWS Fargate Collector is missing requirements and cannot monitor this environment." + ) return super(AWSFargateCollector, self).start() @@ -124,7 +128,9 @@ def get_ecs_metadata(self): # Response from the last call to # ${ECS_CONTAINER_METADATA_URI}/task/stats - json_body = self.http_client.get(self.ecmu_url_task_stats, timeout=1).content + json_body = self.http_client.get( + self.ecmu_url_task_stats, timeout=1 + ).content self.task_stats_metadata = json.loads(json_body) except Exception: logger.debug("AWSFargateCollector.get_ecs_metadata", exc_info=True) diff --git a/src/instana/collector/aws_lambda.py b/src/instana/collector/aws_lambda.py index 5a21da74..c0e4b731 100644 --- a/src/instana/collector/aws_lambda.py +++ b/src/instana/collector/aws_lambda.py @@ -4,15 +4,17 @@ """ AWS Lambda Collector: Manages the periodic collection of metrics & snapshot data """ + +from instana.collector.base import BaseCollector from instana.collector.utils import format_trace_and_span_ids -from ..log import logger -from .base import BaseCollector -from ..util import DictionaryOfStan -from ..util.aws import normalize_aws_lambda_arn +from instana.log import logger +from instana.util import DictionaryOfStan +from instana.util.aws import normalize_aws_lambda_arn class AWSLambdaCollector(BaseCollector): - """ Collector for AWS Lambda """ + """Collector for AWS Lambda""" + def __init__(self, agent): super(AWSLambdaCollector, self).__init__(agent) logger.debug("Loading AWS Lambda Collector") @@ -61,8 +63,10 @@ def get_fq_arn(self): return self._fq_arn if self.context is None: - logger.debug("Attempt to get qualified ARN before the context object is available") - return '' + logger.debug( + "Attempt to get qualified ARN before the context object is available" + ) + return "" self._fq_arn = normalize_aws_lambda_arn(self.context) return self._fq_arn diff --git a/src/instana/collector/base.py b/src/instana/collector/base.py index 009fe3be..8c6df344 100644 --- a/src/instana/collector/base.py +++ b/src/instana/collector/base.py @@ -5,23 +5,24 @@ A Collector launches a background thread and continually collects & reports data. The data can be any combination of metrics, snapshot data and spans. """ + +import queue # pylint: disable=import-error import threading from os import environ -from ..log import logger -from ..util import every, DictionaryOfStan - - -import queue # pylint: disable=import-error +from instana.log import logger +from instana.util import DictionaryOfStan, every # TODO: Use mock.patch() or unittest.mock to mock the testing env env_is_test = "INSTANA_TEST" in environ + class BaseCollector(object): """ Base class to handle the collection & reporting of snapshot and metric data This class launches a background thread to do this work. """ + def __init__(self, agent): # The agent for this process. Can be Standard, AWSLambda or Fargate self.agent = agent @@ -36,6 +37,7 @@ def __init__(self, agent): # others in background processes. This multiprocessing queue allows us to collect # up spans from all sources. import multiprocessing + self.span_queue = multiprocessing.Queue() else: self.span_queue = queue.Queue() @@ -93,7 +95,10 @@ def start(self): timer.name = "Collector Timed Start" timer.start() return - logger.debug("BaseCollector.start non-fatal: call but thread already running (started: %s)", self.started) + logger.debug( + "BaseCollector.start non-fatal: call but thread already running (started: %s)", + self.started, + ) return if self.agent.can_send(): @@ -105,7 +110,9 @@ def start(self): self.reporting_thread.start() self.started = True else: - logger.warning("BaseCollector.start: the agent tells us we can't send anything out") + logger.warning( + "BaseCollector.start: the agent tells us we can't send anything out" + ) def shutdown(self, report_final=True): """ @@ -124,7 +131,11 @@ def thread_loop(self): Just a loop that is run in the background thread. @return: None """ - every(self.report_interval, self.background_report, "Instana Collector: prepare_and_report_data") + every( + self.report_interval, + self.background_report, + "Instana Collector: prepare_and_report_data", + ) def background_report(self): """ @@ -132,13 +143,17 @@ def background_report(self): @return: Boolean """ if self.thread_shutdown.is_set(): - logger.debug("Thread shutdown signal is active: Shutting down reporting thread") + logger.debug( + "Thread shutdown signal is active: Shutting down reporting thread" + ) return False self.prepare_and_report_data() if self.thread_shutdown.is_set(): - logger.debug("Thread shutdown signal is active: Shutting down reporting thread") + logger.debug( + "Thread shutdown signal is active: Shutting down reporting thread" + ) return False return True @@ -189,7 +204,6 @@ def queued_spans(self): spans.append(span) return spans - def queued_profiles(self): """ Get all of the queued profiles diff --git a/src/instana/collector/google_cloud_run.py b/src/instana/collector/google_cloud_run.py index eba5ab7f..ffcbd984 100644 --- a/src/instana/collector/google_cloud_run.py +++ b/src/instana/collector/google_cloud_run.py @@ -4,20 +4,24 @@ """ Google Cloud Run Collector: Manages the periodic collection of metrics & snapshot data """ + import os from time import time + import requests +from instana.collector.base import BaseCollector +from instana.collector.helpers.google_cloud_run.instance_entity import ( + InstanceEntityHelper, +) +from instana.collector.helpers.google_cloud_run.process import GCRProcessHelper from instana.collector.utils import format_trace_and_span_ids from instana.log import logger -from instana.collector.base import BaseCollector from instana.util import DictionaryOfStan, validate_url -from instana.collector.helpers.google_cloud_run.process import GCRProcessHelper -from instana.collector.helpers.google_cloud_run.instance_entity import InstanceEntityHelper class GCRCollector(BaseCollector): - """ Collector for Google Cloud Run """ + """Collector for Google Cloud Run""" def __init__(self, agent, service, configuration, revision): super(GCRCollector, self).__init__(agent) @@ -30,15 +34,23 @@ def __init__(self, agent, service, configuration, revision): self.service = service self.configuration = configuration # Prepare the URLS that we will collect data from - self._gcr_md_uri = os.environ.get("GOOGLE_CLOUD_RUN_METADATA_ENDPOINT", "http://metadata.google.internal") + self._gcr_md_uri = os.environ.get( + "GOOGLE_CLOUD_RUN_METADATA_ENDPOINT", "http://metadata.google.internal" + ) if self._gcr_md_uri == "" or validate_url(self._gcr_md_uri) is False: - logger.warning("GCRCollector: GOOGLE_CLOUD_RUN_METADATA_ENDPOINT not in environment or invalid URL. " - "Instana will not be able to monitor this environment") + logger.warning( + "GCRCollector: GOOGLE_CLOUD_RUN_METADATA_ENDPOINT not in environment or invalid URL. " + "Instana will not be able to monitor this environment" + ) self.ready_to_start = False - self._gcr_md_project_uri = self._gcr_md_uri + '/computeMetadata/v1/project/?recursive=true' - self._gcr_md_instance_uri = self._gcr_md_uri + '/computeMetadata/v1/instance/?recursive=true' + self._gcr_md_project_uri = ( + self._gcr_md_uri + "/computeMetadata/v1/project/?recursive=true" + ) + self._gcr_md_instance_uri = ( + self._gcr_md_uri + "/computeMetadata/v1/instance/?recursive=true" + ) # Timestamp in seconds of the last time we fetched all GCR metadata self.__last_gcr_md_full_fetch = 0 @@ -66,7 +78,9 @@ def __init__(self, agent, service, configuration, revision): def start(self): if self.ready_to_start is False: - logger.warning("Google Cloud Run Collector is missing requirements and cannot monitor this environment.") + logger.warning( + "Google Cloud Run Collector is missing requirements and cannot monitor this environment." + ) return super(GCRCollector, self).start() @@ -82,15 +96,19 @@ def __get_project_instance_metadata(self): headers = {"Metadata-Flavor": "Google"} # Response from the last call to # ${GOOGLE_CLOUD_RUN_METADATA_ENDPOINT}/computeMetadata/v1/project/?recursive=true - self.project_metadata = self._http_client.get(self._gcr_md_project_uri, timeout=1, - headers=headers).json() + self.project_metadata = self._http_client.get( + self._gcr_md_project_uri, timeout=1, headers=headers + ).json() # Response from the last call to # ${GOOGLE_CLOUD_RUN_METADATA_ENDPOINT}/computeMetadata/v1/instance/?recursive=true - self.instance_metadata = self._http_client.get(self._gcr_md_instance_uri, timeout=1, - headers=headers).json() + self.instance_metadata = self._http_client.get( + self._gcr_md_instance_uri, timeout=1, headers=headers + ).json() except Exception: - logger.debug("GoogleCloudRunCollector.get_project_instance_metadata", exc_info=True) + logger.debug( + "GoogleCloudRunCollector.get_project_instance_metadata", exc_info=True + ) def should_send_snapshot_data(self): return int(time()) - self.snapshot_data_last_sent > self.snapshot_data_interval @@ -101,7 +119,6 @@ def prepare_payload(self): payload["metrics"]["plugins"] = [] try: - if not self.span_queue.empty(): payload["spans"] = format_trace_and_span_ids(self.queued_spans()) @@ -120,8 +137,12 @@ def prepare_payload(self): plugins = [] for helper in self.helpers: plugins.extend( - helper.collect_metrics(with_snapshot=with_snapshot, instance_metadata=self.instance_metadata, - project_metadata=self.project_metadata)) + helper.collect_metrics( + with_snapshot=with_snapshot, + instance_metadata=self.instance_metadata, + project_metadata=self.project_metadata, + ) + ) payload["metrics"]["plugins"] = plugins diff --git a/src/instana/collector/helpers/base.py b/src/instana/collector/helpers/base.py index 682617a2..e2cea3d5 100644 --- a/src/instana/collector/helpers/base.py +++ b/src/instana/collector/helpers/base.py @@ -6,13 +6,15 @@ in the data collection for various entities such as host, hardware, AWS Task, ec2, memory, cpu, docker etc etc.. """ -from ...log import logger + +from instana.log import logger class BaseHelper(object): """ Base class for all helpers. Descendants must override and implement `self.collect_metrics`. """ + def __init__(self, collector): self.collector = collector @@ -73,6 +75,6 @@ def apply_delta(self, source, previous, new, metric, with_snapshot): if previous_value != new_value or with_snapshot is True: previous[dst_metric] = new[dst_metric] = new_value - + def collect_metrics(self, **kwargs): logger.debug("BaseHelper.collect_metrics must be overridden") diff --git a/src/instana/collector/helpers/eks/process.py b/src/instana/collector/helpers/eks/process.py index 09198532..86239520 100644 --- a/src/instana/collector/helpers/eks/process.py +++ b/src/instana/collector/helpers/eks/process.py @@ -1,13 +1,15 @@ # (c) Copyright IBM Corp. 2024 -""" Module to handle the collection of containerized process metrics for EKS Pods on AWS Fargate """ +"""Module to handle the collection of containerized process metrics for EKS Pods on AWS Fargate""" + import os + from instana.collector.helpers.process import ProcessHelper from instana.log import logger def get_pod_name(): - podname = os.environ.get('HOSTNAME', '') + podname = os.environ.get("HOSTNAME", "") if not podname: logger.warning("Failed to determine podname from EKS hostname.") @@ -15,7 +17,7 @@ def get_pod_name(): class EKSFargateProcessHelper(ProcessHelper): - """ Helper class to extend the generic process helper class with the corresponding fargate attributes """ + """Helper class to extend the generic process helper class with the corresponding fargate attributes""" def collect_metrics(self, **kwargs): plugin_data = dict() diff --git a/src/instana/collector/helpers/fargate/container.py b/src/instana/collector/helpers/fargate/container.py index 90981298..82ad7bea 100644 --- a/src/instana/collector/helpers/fargate/container.py +++ b/src/instana/collector/helpers/fargate/container.py @@ -1,14 +1,16 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -""" Module to handle the collection of container metrics in AWS Fargate """ -from ....log import logger -from ....util import DictionaryOfStan -from ..base import BaseHelper +"""Module to handle the collection of container metrics in AWS Fargate""" + +from instana.collector.helpers.base import BaseHelper +from instana.log import logger +from instana.util import DictionaryOfStan class ContainerHelper(BaseHelper): - """ This class acts as a helper to collect container snapshot and metric information """ + """This class acts as a helper to collect container snapshot and metric information""" + def collect_metrics(self, **kwargs): """ Collect and return metrics (and optionally snapshot data) for every container in this task @@ -31,27 +33,55 @@ def collect_metrics(self, **kwargs): plugin_data["data"] = DictionaryOfStan() if self.collector.root_metadata["Name"] == name: plugin_data["data"]["instrumented"] = True - plugin_data["data"]["dockerId"] = container.get("DockerId", None) - plugin_data["data"]["taskArn"] = labels.get("com.amazonaws.ecs.task-arn", None) + plugin_data["data"]["dockerId"] = container.get( + "DockerId", None + ) + plugin_data["data"]["taskArn"] = labels.get( + "com.amazonaws.ecs.task-arn", None + ) if kwargs.get("with_snapshot"): plugin_data["data"]["runtime"] = "python" - plugin_data["data"]["dockerName"] = container.get("DockerName", None) - plugin_data["data"]["containerName"] = container.get("Name", None) + plugin_data["data"]["dockerName"] = container.get( + "DockerName", None + ) + plugin_data["data"]["containerName"] = container.get( + "Name", None + ) plugin_data["data"]["image"] = container.get("Image", None) - plugin_data["data"]["imageId"] = container.get("ImageID", None) - plugin_data["data"]["taskDefinition"] = labels.get("com.amazonaws.ecs.task-definition-family", None) - plugin_data["data"]["taskDefinitionVersion"] = labels.get("com.amazonaws.ecs.task-definition-version", None) - plugin_data["data"]["clusterArn"] = labels.get("com.amazonaws.ecs.cluster", None) - plugin_data["data"]["desiredStatus"] = container.get("DesiredStatus", None) - plugin_data["data"]["knownStatus"] = container.get("KnownStatus", None) + plugin_data["data"]["imageId"] = container.get( + "ImageID", None + ) + plugin_data["data"]["taskDefinition"] = labels.get( + "com.amazonaws.ecs.task-definition-family", None + ) + plugin_data["data"]["taskDefinitionVersion"] = labels.get( + "com.amazonaws.ecs.task-definition-version", None + ) + plugin_data["data"]["clusterArn"] = labels.get( + "com.amazonaws.ecs.cluster", None + ) + plugin_data["data"]["desiredStatus"] = container.get( + "DesiredStatus", None + ) + plugin_data["data"]["knownStatus"] = container.get( + "KnownStatus", None + ) plugin_data["data"]["ports"] = container.get("Ports", None) - plugin_data["data"]["createdAt"] = container.get("CreatedAt", None) - plugin_data["data"]["startedAt"] = container.get("StartedAt", None) + plugin_data["data"]["createdAt"] = container.get( + "CreatedAt", None + ) + plugin_data["data"]["startedAt"] = container.get( + "StartedAt", None + ) plugin_data["data"]["type"] = container.get("Type", None) limits = container.get("Limits", {}) - plugin_data["data"]["limits"]["cpu"] = limits.get("CPU", None) - plugin_data["data"]["limits"]["memory"] = limits.get("Memory", None) + plugin_data["data"]["limits"]["cpu"] = limits.get( + "CPU", None + ) + plugin_data["data"]["limits"]["memory"] = limits.get( + "Memory", None + ) except Exception: logger.debug("_collect_container_snapshots: ", exc_info=True) finally: diff --git a/src/instana/collector/helpers/process.py b/src/instana/collector/helpers/process.py index 073842f2..2f2115cb 100644 --- a/src/instana/collector/helpers/process.py +++ b/src/instana/collector/helpers/process.py @@ -1,19 +1,21 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -""" Collection helper for the process """ +"""Collection helper for the process""" + +import grp import os import pwd -import grp + +from instana.collector.helpers.base import BaseHelper from instana.log import logger from instana.util import DictionaryOfStan from instana.util.runtime import get_proc_cmdline from instana.util.secrets import contains_secret -from .base import BaseHelper class ProcessHelper(BaseHelper): - """ Helper class to collect metrics for this process """ + """Helper class to collect metrics for this process""" def collect_metrics(self, **kwargs): plugin_data = dict() @@ -33,9 +35,11 @@ def _collect_process_snapshot(self, plugin_data): try: env = dict() for key in os.environ: - if contains_secret(key, - self.collector.agent.options.secrets_matcher, - self.collector.agent.options.secrets_list): + if contains_secret( + key, + self.collector.agent.options.secrets_matcher, + self.collector.agent.options.secrets_list, + ): env[key] = "" else: env[key] = os.environ[key] diff --git a/src/instana/collector/helpers/runtime.py b/src/instana/collector/helpers/runtime.py index f6111bb0..2ea68642 100644 --- a/src/instana/collector/helpers/runtime.py +++ b/src/instana/collector/helpers/runtime.py @@ -4,19 +4,19 @@ """ Collection helper for the Python runtime """ import importlib.metadata import os -import gc -import sys import platform import resource +import sys import threading from types import ModuleType +from instana.collector.helpers.base import BaseHelper from instana.log import logger -from instana.version import VERSION from instana.util import DictionaryOfStan from instana.util.runtime import determine_service_name +from instana.version import VERSION -from .base import BaseHelper +PATH_OF_DEPRECATED_INSTALLATION_VIA_HOST_AGENT = "/tmp/.instana/python" PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR = '/opt/instana/instrumentation/python/' @@ -29,7 +29,7 @@ def is_webhook_instrumented(): class RuntimeHelper(BaseHelper): - """ Helper class to collect snapshot and metrics for this Python runtime """ + """Helper class to collect snapshot and metrics for this Python runtime""" def __init__(self, collector): super(RuntimeHelper, self).__init__(collector) @@ -66,7 +66,7 @@ def collect_metrics(self, **kwargs): return [plugin_data] def _collect_runtime_metrics(self, plugin_data, with_snapshot): - if os.environ.get('INSTANA_DISABLE_METRICS_COLLECTION', False): + if os.environ.get("INSTANA_DISABLE_METRICS_COLLECTION", False): return """ Collect up and return the runtime metrics """ @@ -78,61 +78,141 @@ def _collect_runtime_metrics(self, plugin_data, with_snapshot): self._collect_thread_metrics(plugin_data, with_snapshot) value_diff = rusage.ru_utime - self.previous_rusage.ru_utime - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_utime", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_utime", + with_snapshot, + ) value_diff = rusage.ru_stime - self.previous_rusage.ru_stime - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_stime", with_snapshot) - - self.apply_delta(rusage.ru_maxrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_maxrss", with_snapshot) - self.apply_delta(rusage.ru_ixrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_ixrss", with_snapshot) - self.apply_delta(rusage.ru_idrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_idrss", with_snapshot) - self.apply_delta(rusage.ru_isrss, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_isrss", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_stime", + with_snapshot, + ) + + self.apply_delta( + rusage.ru_maxrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_maxrss", + with_snapshot, + ) + self.apply_delta( + rusage.ru_ixrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_ixrss", + with_snapshot, + ) + self.apply_delta( + rusage.ru_idrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_idrss", + with_snapshot, + ) + self.apply_delta( + rusage.ru_isrss, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_isrss", + with_snapshot, + ) value_diff = rusage.ru_minflt - self.previous_rusage.ru_minflt - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_minflt", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_minflt", + with_snapshot, + ) value_diff = rusage.ru_majflt - self.previous_rusage.ru_majflt - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_majflt", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_majflt", + with_snapshot, + ) value_diff = rusage.ru_nswap - self.previous_rusage.ru_nswap - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nswap", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nswap", + with_snapshot, + ) value_diff = rusage.ru_inblock - self.previous_rusage.ru_inblock - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_inblock", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_inblock", + with_snapshot, + ) value_diff = rusage.ru_oublock - self.previous_rusage.ru_oublock - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_oublock", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_oublock", + with_snapshot, + ) value_diff = rusage.ru_msgsnd - self.previous_rusage.ru_msgsnd - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_msgsnd", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_msgsnd", + with_snapshot, + ) value_diff = rusage.ru_msgrcv - self.previous_rusage.ru_msgrcv - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_msgrcv", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_msgrcv", + with_snapshot, + ) value_diff = rusage.ru_nsignals - self.previous_rusage.ru_nsignals - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nsignals", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nsignals", + with_snapshot, + ) value_diff = rusage.ru_nvcsw - self.previous_rusage.ru_nvcsw - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nvcsw", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nvcsw", + with_snapshot, + ) value_diff = rusage.ru_nivcsw - self.previous_rusage.ru_nivcsw - self.apply_delta(value_diff, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "ru_nivcsw", with_snapshot) + self.apply_delta( + value_diff, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "ru_nivcsw", + with_snapshot, + ) except Exception: logger.debug("_collect_runtime_metrics", exc_info=True) finally: @@ -143,19 +223,49 @@ def _collect_gc_metrics(self, plugin_data, with_snapshot): gc_count = gc.get_count() gc_threshold = gc.get_threshold() - self.apply_delta(gc_count[0], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "collect0", with_snapshot) - self.apply_delta(gc_count[1], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "collect1", with_snapshot) - self.apply_delta(gc_count[2], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "collect2", with_snapshot) - - self.apply_delta(gc_threshold[0], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "threshold0", with_snapshot) - self.apply_delta(gc_threshold[1], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "threshold1", with_snapshot) - self.apply_delta(gc_threshold[2], self.previous['data']['metrics']['gc'], - plugin_data['data']['metrics']['gc'], "threshold2", with_snapshot) + self.apply_delta( + gc_count[0], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "collect0", + with_snapshot, + ) + self.apply_delta( + gc_count[1], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "collect1", + with_snapshot, + ) + self.apply_delta( + gc_count[2], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "collect2", + with_snapshot, + ) + + self.apply_delta( + gc_threshold[0], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "threshold0", + with_snapshot, + ) + self.apply_delta( + gc_threshold[1], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "threshold1", + with_snapshot, + ) + self.apply_delta( + gc_threshold[2], + self.previous["data"]["metrics"]["gc"], + plugin_data["data"]["metrics"]["gc"], + "threshold2", + with_snapshot, + ) except Exception: logger.debug("_collect_gc_metrics", exc_info=True) @@ -163,55 +273,77 @@ def _collect_thread_metrics(self, plugin_data, with_snapshot): try: threads = threading.enumerate() daemon_threads = [thread.daemon is True for thread in threads].count(True) - self.apply_delta(daemon_threads, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "daemon_threads", with_snapshot) + self.apply_delta( + daemon_threads, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "daemon_threads", + with_snapshot, + ) alive_threads = [thread.daemon is False for thread in threads].count(True) - self.apply_delta(alive_threads, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "alive_threads", with_snapshot) - - dummy_threads = [isinstance(thread, threading._DummyThread) for thread in threads].count( - True) # pylint: disable=protected-access - self.apply_delta(dummy_threads, self.previous['data']['metrics'], - plugin_data['data']['metrics'], "dummy_threads", with_snapshot) + self.apply_delta( + alive_threads, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "alive_threads", + with_snapshot, + ) + + dummy_threads = [ + isinstance(thread, threading._DummyThread) for thread in threads + ].count(True) # pylint: disable=protected-access + self.apply_delta( + dummy_threads, + self.previous["data"]["metrics"], + plugin_data["data"]["metrics"], + "dummy_threads", + with_snapshot, + ) except Exception: logger.debug("_collect_thread_metrics", exc_info=True) def _collect_runtime_snapshot(self, plugin_data): - """ Gathers Python specific Snapshot information for this process """ + """Gathers Python specific Snapshot information for this process""" snapshot_payload = {} try: - snapshot_payload['name'] = determine_service_name() - snapshot_payload['version'] = sys.version - snapshot_payload['f'] = platform.python_implementation() # flavor - snapshot_payload['a'] = platform.architecture()[0] # architecture - snapshot_payload['versions'] = self.gather_python_packages() - snapshot_payload['iv'] = VERSION + snapshot_payload["name"] = determine_service_name() + snapshot_payload["version"] = sys.version + snapshot_payload["f"] = platform.python_implementation() # flavor + snapshot_payload["a"] = platform.architecture()[0] # architecture + snapshot_payload["versions"] = self.gather_python_packages() + snapshot_payload["iv"] = VERSION if is_autowrapt_instrumented(): snapshot_payload['m'] = 'Autowrapt' elif is_webhook_instrumented(): snapshot_payload['m'] = 'AutoTrace' else: - snapshot_payload['m'] = 'Manual' + snapshot_payload["m"] = "Manual" try: - from django.conf import settings # pylint: disable=import-outside-toplevel - if hasattr(settings, 'MIDDLEWARE') and settings.MIDDLEWARE is not None: - snapshot_payload['djmw'] = settings.MIDDLEWARE - elif hasattr(settings, 'MIDDLEWARE_CLASSES') and settings.MIDDLEWARE_CLASSES is not None: - snapshot_payload['djmw'] = settings.MIDDLEWARE_CLASSES + from django.conf import ( + settings, # pylint: disable=import-outside-toplevel + ) + + if hasattr(settings, "MIDDLEWARE") and settings.MIDDLEWARE is not None: + snapshot_payload["djmw"] = settings.MIDDLEWARE + elif ( + hasattr(settings, "MIDDLEWARE_CLASSES") + and settings.MIDDLEWARE_CLASSES is not None + ): + snapshot_payload["djmw"] = settings.MIDDLEWARE_CLASSES except Exception: pass except Exception: logger.debug("collect_snapshot: ", exc_info=True) - plugin_data['data']['snapshot'] = snapshot_payload + plugin_data["data"]["snapshot"] = snapshot_payload def gather_python_packages(self): - """ Collect up the list of modules in use """ - if os.environ.get('INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION'): - return {'instana': VERSION} + """Collect up the list of modules in use""" + if os.environ.get("INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"): + return {"instana": VERSION} versions = {} try: @@ -220,7 +352,7 @@ def gather_python_packages(self): for pkg_name in sys_packages: # Don't report submodules (e.g. django.x, django.y, django.z) # Skip modules that begin with underscore - if ('.' in pkg_name) or pkg_name[0] == '_': + if ("." in pkg_name) or pkg_name[0] == "_": continue # Skip builtins @@ -234,7 +366,9 @@ def gather_python_packages(self): if isinstance(pkg_info["__version__"], str): versions[pkg_name] = pkg_info["__version__"] else: - versions[pkg_name] = self.jsonable(pkg_info["__version__"]) + versions[pkg_name] = self.jsonable( + pkg_info["__version__"] + ) elif "version" in pkg_info: versions[pkg_name] = self.jsonable(pkg_info["version"]) else: @@ -242,10 +376,13 @@ def gather_python_packages(self): except importlib.metadata.PackageNotFoundError: pass except Exception: - logger.debug("gather_python_packages: could not process module: %s", pkg_name) + logger.debug( + "gather_python_packages: could not process module: %s", + pkg_name, + ) # Manually set our package version - versions['instana'] = VERSION + versions["instana"] = VERSION except Exception: logger.debug("gather_python_packages", exc_info=True) @@ -257,7 +394,7 @@ def jsonable(self, value): try: result = value() except Exception: - result = 'Unknown' + result = "Unknown" elif isinstance(value, ModuleType): result = value else: diff --git a/src/instana/collector/host.py b/src/instana/collector/host.py index 6b09a7eb..d415dfde 100644 --- a/src/instana/collector/host.py +++ b/src/instana/collector/host.py @@ -4,6 +4,7 @@ """ Host Collector: Manages the periodic collection of metrics & snapshot data """ + from time import time from instana.collector.base import BaseCollector @@ -14,8 +15,9 @@ class HostCollector(BaseCollector): - """ Collector for host agent """ - def __init__(self, agent): + """Collector for host agent""" + + def __init__(self, agent) -> None: super(HostCollector, self).__init__(agent) logger.debug("Loading Host Collector") @@ -25,14 +27,16 @@ def __init__(self, agent): # Populate the collection helpers self.helpers.append(RuntimeHelper(self)) - def start(self): + def start(self) -> None: if self.ready_to_start is False: - logger.warning("Host Collector is missing requirements and cannot monitor this environment.") + logger.warning( + "Host Collector is missing requirements and cannot monitor this environment." + ) return super(HostCollector, self).start() - def prepare_and_report_data(self): + def prepare_and_report_data(self) -> None: """ We override this method from the base class so that we can handle the wait4init state machine case. @@ -47,21 +51,28 @@ def prepare_and_report_data(self): else: return - if self.agent.machine.fsm.current == "good2go" and self.agent.is_timed_out(): - logger.info("The Instana host agent has gone offline or is no longer reachable for > 1 min. Will retry periodically.") + if ( + self.agent.machine.fsm.current == "good2go" + and self.agent.is_timed_out() + ): + logger.info( + "The Instana host agent has gone offline or is no longer reachable for > 1 min. Will retry periodically." + ) self.agent.reset() except Exception: - logger.debug('Harmless state machine thread disagreement. Will self-correct on next timer cycle.') + logger.debug( + "Harmless state machine thread disagreement. Will self-correct on next timer cycle." + ) super(HostCollector, self).prepare_and_report_data() - def should_send_snapshot_data(self): + def should_send_snapshot_data(self) -> bool: delta = int(time()) - self.snapshot_data_last_sent if delta > self.snapshot_data_interval: return True return False - def prepare_payload(self): + def prepare_payload(self) -> DictionaryOfStan: payload = DictionaryOfStan() payload["spans"] = [] payload["profiles"] = [] From 5ba7358d61cbb5e1b4512406108cf3d395bf2e59 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 18 Apr 2024 17:17:27 +0200 Subject: [PATCH 020/172] test(OTel): Add tests for SpanContext and Event classes. Signed-off-by: Paulo Vital --- tests/test_span_context.py | 66 ++++++++++++++++++++++++++++++++++++++ tests/test_span_event.py | 35 ++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 tests/test_span_context.py create mode 100644 tests/test_span_event.py diff --git a/tests/test_span_context.py b/tests/test_span_context.py new file mode 100644 index 00000000..517007f8 --- /dev/null +++ b/tests/test_span_context.py @@ -0,0 +1,66 @@ +# (c) Copyright IBM Corp. 2024 + +import pickle +from opentelemetry.trace.span import ( + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + format_span_id, +) + +from instana.span_context import SpanContext +from instana.util.ids import generate_id + + +def test_span_context_defaults(): + trace_id = generate_id() + span_id = generate_id() + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + ) + + assert isinstance(span_context, SpanContext) + assert span_context.trace_id == trace_id + assert span_context.span_id == span_id + assert span_context.trace_id != span_context.span_id + assert not span_context.is_remote + assert span_context.trace_flags == DEFAULT_TRACE_OPTIONS + assert span_context.trace_state == DEFAULT_TRACE_STATE + assert span_context.is_valid + assert span_context.level == 1 + assert not span_context.synthetic + assert span_context.trace_parent is None + assert span_context.instana_ancestor is None + assert span_context.long_trace_id is None + assert span_context.correlation_type is None + assert span_context.correlation_id is None + assert span_context.traceparent is None + assert span_context.tracestate is None + assert not span_context.suppression + assert repr(span_context) == f"SpanContext(trace_id=0x{format_span_id(trace_id)}, span_id=0x{format_span_id(span_id)}, trace_flags=0x{DEFAULT_TRACE_OPTIONS:02x}, trace_state={DEFAULT_TRACE_STATE!r}, is_remote=False, synthetic=False)" + + +def test_span_context_invalid(): + span_context = SpanContext( + trace_id=9999999999999999999999999999999999999999999999999999999999999999999999999999, + span_id=9, + is_remote=False, + ) + assert not span_context.is_valid + + +def test_span_context_pickle(): + trace_id = generate_id() + span_id = generate_id() + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + ) + + span_context_binary = pickle.dumps(span_context) + span_context_pickle = pickle.loads(span_context_binary) + assert trace_id == span_context_pickle.trace_id + assert span_id == span_context_pickle.span_id + diff --git a/tests/test_span_event.py b/tests/test_span_event.py new file mode 100644 index 00000000..cdfc724a --- /dev/null +++ b/tests/test_span_event.py @@ -0,0 +1,35 @@ +# (c) Copyright IBM Corp. 2024 + +import time +from instana.span import Event + + +def test_span_event_defaults(): + event_name = "test-span-event" + event = Event(event_name) + + assert event + assert isinstance(event, Event) + assert event.name == event_name + assert not event.attributes + assert isinstance(event.timestamp, int) + + +def test_span_event(): + event_name = "test-span-event" + attributes = { + "field1": 1, + "field2": "two", + } + timestamp = time.time_ns() + + event = Event(event_name, attributes, timestamp) + + assert event + assert isinstance(event, Event) + assert event.name == event_name + assert event.attributes + assert len(event.attributes) == 2 + assert "field1" in event.attributes.keys() + assert "two" == event.attributes.get("field2") + assert event.timestamp == timestamp From fb0d1888960ee819aa02909dd91dc1a88fa15aab Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 29 Apr 2024 16:31:23 +0200 Subject: [PATCH 021/172] tests(OTel): Add tests for InstanaSpan, BaseSpan, SDKSpan and RegisteredSpan. Signed-off-by: Paulo Vital --- tests/conftest.py | 37 ++ tests/test_span.py | 727 ++++++++++++++++++++++++++++++++++ tests/test_span_base.py | 157 ++++++++ tests/test_span_registered.py | 408 +++++++++++++++++++ tests/test_span_sdk.py | 83 ++++ 5 files changed, 1412 insertions(+) create mode 100644 tests/test_span.py create mode 100644 tests/test_span_base.py create mode 100644 tests/test_span_registered.py create mode 100644 tests/test_span_sdk.py diff --git a/tests/conftest.py b/tests/conftest.py index 48a30a2e..901be6ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,13 @@ os.environ["INSTANA_TEST"] = "true" os.environ["INSTANA_DISABLE_AUTO_INSTR"] = "true" +# TODO: remove all "noqa: E402" from instana package imports and move the +# block of env variables setting to below the imports after finishing the +# migration of instrumentation codes. +from instana.span import BaseSpan, InstanaSpan # noqa: E402 +from instana.span_context import SpanContext # noqa: E402 + + collect_ignore_glob = [ "*autoprofile*", "*clients*", @@ -91,3 +98,33 @@ def celery_enable_logging(): @pytest.fixture(scope="session") def celery_includes(): return {"tests.frameworks.test_celery"} + + +@pytest.fixture +def trace_id() -> int: + return 1812338823475918251 + + +@pytest.fixture +def span_id() -> int: + return 6895521157646639861 + + +@pytest.fixture +def span_context(trace_id: int, span_id: int) -> SpanContext: + return SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + ) + + +@pytest.fixture +def span(span_context: SpanContext) -> InstanaSpan: + span_name = "test-span" + return InstanaSpan(span_name, span_context) + + +@pytest.fixture +def base_span(span: InstanaSpan) -> BaseSpan: + return BaseSpan(span, None, "test") diff --git a/tests/test_span.py b/tests/test_span.py new file mode 100644 index 00000000..5f30f9f1 --- /dev/null +++ b/tests/test_span.py @@ -0,0 +1,727 @@ +# (c) Copyright IBM Corp. 2024 + +import time +from unittest.mock import patch + +import pytest +from opentelemetry.trace.status import Status, StatusCode + +from instana.span import INVALID_SPAN, Event, InstanaSpan, get_current_span +from instana.span_context import SpanContext + + +def test_span_default( + span_context: SpanContext, + trace_id: int, + span_id: int, +) -> None: + span_name = "test-span" + timestamp = time.time_ns() + span = InstanaSpan(span_name, span_context) + + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == span_name + + context = span.context + assert isinstance(context, SpanContext) + assert context.trace_id == trace_id + assert context.span_id == span_id + + assert span.start_time + assert isinstance(span.start_time, int) + assert span.start_time > timestamp + assert not span.end_time + assert not span.attributes + assert not span.events + assert span.is_recording() + assert span.status + assert span.status.is_unset + + +def test_span_get_span_context( + span_context: SpanContext, + trace_id: int, + span_id: int, +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + context = span.get_span_context() + assert isinstance(context, SpanContext) + assert context.trace_id == trace_id + assert context.span_id == span_id + assert context == span.context + + +def test_span_set_attributes_default(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert not span.attributes + + attributes = { + "field1": 1, + "field2": "two", + } + span.set_attributes(attributes) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + +def test_span_set_attributes(span_context: SpanContext) -> None: + span_name = "test-span" + attributes = { + "field1": 1, + "field2": "two", + } + span = InstanaSpan(span_name, span_context, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + span.set_attributes(attributes) + + assert len(span.attributes) == 4 + assert "field3" in span.attributes.keys() + assert "vier" in span.attributes.get("field4") + + +def test_span_set_attribute_default(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert not span.attributes + + attributes = { + "field1": 1, + "field2": "two", + } + for key, value in attributes.items(): + span.set_attribute(key, value) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + +def test_span_set_attribute(span_context: SpanContext) -> None: + span_name = "test-span" + attributes = { + "field1": 1, + "field2": "two", + } + span = InstanaSpan(span_name, span_context, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 2 + assert "field1" in span.attributes.keys() + assert "two" == span.attributes.get("field2") + + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + for key, value in attributes.items(): + span.set_attribute(key, value) + + assert len(span.attributes) == 4 + assert "field3" in span.attributes.keys() + assert "vier" in span.attributes.get("field4") + + +def test_span_update_name(span_context: SpanContext) -> None: + span_name = "test-span-1" + span = InstanaSpan(span_name, span_context) + + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == span_name + + new_span_name = "test-span-2" + span.update_name(new_span_name) + assert span is not None + assert isinstance(span, InstanaSpan) + assert span.name == new_span_name + + +def test_span_set_status_with_Status_default(span_context, caplog) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span.set_status(span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_Status_and_desc(span_context, caplog) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + set_status_desc = "Test" + span.set_status(span_status, set_status_desc) + excepted_log = f"Description {set_status_desc} ignored. Use either `Status` or `(StatusCode, Description)`" + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert excepted_log == caplog.record_tuples[1][2] + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_StatusUNSET_to_StatusERROR(span_context, caplog) -> None: + span_name = "test-span" + status_desc = "Status is UNSET." + span_status = Status(status_code=StatusCode.UNSET, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, status=span_status) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status = Status(StatusCode.ERROR, status_desc) + span.set_status(span_status) + + assert span.status + assert not span.status.is_unset + assert not span.status.is_ok + assert span.status.description == status_desc + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code == StatusCode.ERROR + + +def test_span_set_status_with_StatusOK_to_StatusERROR(span_context, caplog) -> None: + span_name = "test-span" + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, status=span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status = Status(StatusCode.ERROR, status_desc) + span.set_status(span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_StatusCode_default(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + span_status_code = StatusCode(StatusCode.OK) + + span.set_status(span_status_code) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_set_status_with_StatusCode_and_desc(span_context, caplog) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Status is OK." + span_status_code = StatusCode(StatusCode.OK) + span.set_status(span_status_code, status_desc) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + +def test_span_set_status_with_StatusCodeUNSET_to_StatusCodeERROR( + span_context, caplog +) -> None: + span_name = "test-span" + status_desc = "Status is UNSET." + span_status = Status(status_code=StatusCode.UNSET, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, status=span_status) + + assert span.status + assert span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code == StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status_code = StatusCode(StatusCode.ERROR) + span.set_status(span_status_code, status_desc) + + assert span.status + assert not span.status.is_unset + assert not span.status.is_ok + assert span.status.description == status_desc + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code != StatusCode.OK + assert span.status.status_code == StatusCode.ERROR + + +def test_span_set_status_with_StatusCodeOK_to_StatusCodeERROR( + span_context, caplog +) -> None: + span_name = "test-span" + status_desc = "Status is OK." + span_status = Status(status_code=StatusCode.OK, description=status_desc) + + assert ( + "description should only be set when status_code is set to StatusCode.ERROR" + == caplog.record_tuples[0][2] + ) + + span = InstanaSpan(span_name, span_context, status=span_status) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + status_desc = "Houston we have a problem!" + span_status_code = StatusCode(StatusCode.ERROR) + span.set_status(span_status_code, status_desc) + + assert span.status + assert not span.status.is_unset + assert span.status.is_ok + assert not span.status.description + assert span.status.status_code != StatusCode.UNSET + assert span.status.status_code == StatusCode.OK + assert span.status.status_code != StatusCode.ERROR + + +def test_span_add_event_default(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert not span.events + + event_name = "event1" + attributes = { + "field1": 1, + "field2": "two", + } + timestamp = time.time_ns() + span.add_event(event_name, attributes, timestamp) + + assert span.events + assert len(span.events) == 1 + for event in span.events: + assert isinstance(event, Event) + assert event.name == event_name + assert event.timestamp == timestamp + assert len(event.attributes) == 2 + + +def test_span_add_event(span_context: SpanContext) -> None: + span_name = "test-span" + event_name1 = "event1" + attributes = { + "field1": 1, + "field2": "two", + } + timestamp1 = time.time_ns() + event = Event(event_name1, attributes, timestamp1) + span = InstanaSpan(span_name, span_context, events=[event]) + + assert span.events + assert len(span.events) == 1 + for event in span.events: + assert isinstance(event, Event) + assert event.name == event_name1 + assert event.timestamp == timestamp1 + assert len(event.attributes) == 2 + + event_name2 = "event2" + attributes = { + "field3": True, + "field4": ["four", "vier", "quatro"], + } + timestamp2 = time.time_ns() + span.add_event(event_name2, attributes, timestamp2) + + assert len(span.events) == 2 + for event in span.events: + assert isinstance(event, Event) + assert event.name in [event_name1, event_name2] + assert event.timestamp in [timestamp1, timestamp2] + assert len(event.attributes) == 2 + + +@pytest.mark.parametrize( + "span_name, span_attribute", + [ + ("test-span", None), + ("rpc-server", "rpc.error"), + ("rpc-client", "rpc.error"), + ("mysql", "mysql.error"), + ("postgres", "pg.error"), + ("django", "http.error"), + ("http", "http.error"), + ("urllib3", "http.error"), + ("wsgi", "http.error"), + ("asgi", "http.error"), + ("celery-client", "error"), + ("celery-worker", "error"), + ("sqlalchemy", "sqlalchemy.err"), + ("aws.lambda.entry", "lambda.error"), + ], +) +def test_span_record_exception_default( + span_context: SpanContext, + span_name: str, + span_attribute: str, +) -> None: + exception_msg = "Test Exception" + + exception = Exception(exception_msg) + span = InstanaSpan(span_name, span_context) + + span.record_exception(exception) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + if span_attribute: + assert span_attribute in span.attributes.keys() + assert exception_msg == span.attributes.get(span_attribute, None) + else: + event = span.events[-1] # always get the latest event + assert isinstance(event, Event) + assert "exception" == event.name + assert exception_msg == event.attributes.get("message", None) + + +def test_span_record_exception_with_attribute(span_context: SpanContext) -> None: + span_name = "test-span" + exception_msg = "Test Exception" + attributes = { + "custom_attr": 0, + } + + exception = Exception(exception_msg) + span = InstanaSpan(span_name, span_context) + + span.record_exception(exception, attributes) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + + event = span.events[-1] # always get the latest event + assert isinstance(event, Event) + assert 2 == len(event.attributes) + assert exception_msg == event.attributes.get("message", None) + assert 0 == event.attributes.get("custom_attr", None) + + +def test_span_record_exception_with_Exception_msg(span_context: SpanContext) -> None: + span_name = "wsgi" + span_attribute = "http.error" + exception_msg = "Test Exception" + + exception = Exception() + exception.message = exception_msg + span = InstanaSpan(span_name, span_context) + + span.record_exception(exception) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + assert span_attribute in span.attributes.keys() + assert exception_msg == span.attributes.get(span_attribute, None) + + +def test_span_record_exception_with_Exception_none_msg( + span_context: SpanContext, +) -> None: + span_name = "wsgi" + span_attribute = "http.error" + + exception = Exception() + exception.message = None + span = InstanaSpan(span_name, span_context) + + span.record_exception(exception) + + assert span_name == span.name + assert 1 == span.attributes.get("ec", 0) + assert span_attribute in span.attributes.keys() + assert "Exception()" == span.attributes.get(span_attribute, None) + + +def test_span_record_exception_with_Exception_raised(span_context: SpanContext) -> None: + span_name = "test-span" + + exception = None + span = InstanaSpan(span_name, span_context) + + with patch( + "instana.span.InstanaSpan.add_event", side_effect=Exception("mocked error") + ): + with pytest.raises(Exception): + span.record_exception(exception) + + +def test_span_end_default(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert not span.end_time + + span.end() + + assert span.end_time + assert isinstance(span.end_time, int) + assert span.duration + assert isinstance(span.duration, int) + assert span.duration > 0 + + +def test_span_end(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + assert not span.end_time + + timestamp_end = time.time_ns() + span.end(timestamp_end) + + assert span.end_time + assert span.end_time == timestamp_end + assert span.duration + assert isinstance(span.duration, int) + assert span.duration > 0 + assert span.duration == (timestamp_end - span.start_time) + + +def test_span_mark_as_errored_default(span_context: SpanContext) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + span = InstanaSpan(span_name, span_context, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 0 + + span.mark_as_errored() + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 1 + + +def test_span_mark_as_errored(span_context: SpanContext) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + span = InstanaSpan(span_name, span_context, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 0 + + attributes = { + "field1": 1, + "field2": "two", + } + span.mark_as_errored(attributes) + + assert span.attributes + assert len(span.attributes) == 3 + assert span.attributes.get("ec") == 1 + assert "field1" in span.attributes.keys() + assert span.attributes.get("field2") == "two" + + span.mark_as_errored() + + assert span.attributes + assert len(span.attributes) == 3 + assert span.attributes.get("ec") == 2 + assert "field1" in span.attributes.keys() + assert span.attributes.get("field2") == "two" + + +def test_span_mark_as_errored_exception(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + with patch( + "instana.span.InstanaSpan.set_attribute", side_effect=Exception("mocked error") + ): + span.mark_as_errored() + assert not span.attributes + + +def test_span_assure_errored_default(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + span.assure_errored() + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 1 + + +def test_span_assure_errored(span_context: SpanContext) -> None: + span_name = "test-span" + attributes = { + "ec": 0, + } + span = InstanaSpan(span_name, span_context, attributes=attributes) + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 0 + + span.assure_errored() + + assert span.attributes + assert len(span.attributes) == 1 + assert span.attributes.get("ec") == 1 + + +def test_span_assure_errored_exception(span_context: SpanContext) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context) + + with patch( + "instana.span.InstanaSpan.set_attribute", side_effect=Exception("mocked error") + ): + span.assure_errored() + assert not span.attributes + + +def test_get_current_span(span_context) -> None: + # span = get_current_span(span_context) + # assert span + pass + + +def test_get_current_span_INVALID_SPAN() -> None: + span = get_current_span() + + assert span + assert span == INVALID_SPAN diff --git a/tests/test_span_base.py b/tests/test_span_base.py new file mode 100644 index 00000000..f4f8a66c --- /dev/null +++ b/tests/test_span_base.py @@ -0,0 +1,157 @@ +# (c) Copyright IBM Corp. 2024 + +from unittest.mock import Mock, patch + +from instana.span import BaseSpan, InstanaSpan +from instana.span_context import SpanContext +from instana.util import DictionaryOfStan + + +def test_basespan( + span: InstanaSpan, + trace_id: int, + span_id: int, +) -> None: + base_span = BaseSpan(span, None, "test") + + expected_dict = { + "t": trace_id, + "p": None, + "s": span_id, + "l": 1, + "ts": round(span.start_time / 10**6), + "d": round(span.duration / 10**6), + "f": None, + "ec": None, + "data": DictionaryOfStan(), + "stack": None, + } + + assert expected_dict["t"] == base_span.t + assert expected_dict["s"] == base_span.s + assert expected_dict["p"] == base_span.p + assert expected_dict["l"] == base_span.l + assert expected_dict["ts"] == base_span.ts + assert expected_dict["d"] == base_span.d + assert not base_span.f + assert expected_dict["ec"] == base_span.ec + assert isinstance(base_span.data, dict) + assert expected_dict["stack"] == base_span.stack + assert not base_span.sy + + expected_dict_str = str(expected_dict) + assert expected_dict_str == repr(base_span) + assert f"BaseSpan({expected_dict_str})" == str(base_span) + + +def test_basespan_with_synthetic_source_and_kwargs( + span: InstanaSpan, + trace_id: int, + span_id: int, +) -> None: + span.synthetic = True + source = "source test" + _kwarg1 = "value1" + base_span = BaseSpan(span, source, "test", arg1=_kwarg1) + + assert trace_id == base_span.t + assert span_id == base_span.s + assert base_span.sy + assert source == base_span.f + assert _kwarg1 == base_span.arg1 + + +def test_populate_extra_span_attributes(span: InstanaSpan) -> None: + base_span = BaseSpan(span, None, "test") + base_span._populate_extra_span_attributes(span) + + assert not hasattr(base_span, "tp") + assert not hasattr(base_span, "tp") + assert not hasattr(base_span, "ia") + assert not hasattr(base_span, "lt") + assert not hasattr(base_span, "crtp") + assert not hasattr(base_span, "crid") + + +def test_populate_extra_span_attributes_with_values( + trace_id: int, + span_id: int, +) -> None: + long_id = 1512366075204170929049582354406559215 + span_context = SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + synthetic=True, + trace_parent=True, + instana_ancestor="IDK", + long_trace_id=long_id, + correlation_type="IDK", + correlation_id=long_id, + ) + span = InstanaSpan("test-base-span", span_context) + base_span = BaseSpan(span, None, "test") + base_span._populate_extra_span_attributes(span) + + assert trace_id == base_span.t + assert span_id == base_span.s + assert base_span.sy + assert base_span.tp + assert "IDK" == base_span.ia + assert long_id == base_span.lt + assert "IDK" == base_span.crtp + assert long_id == base_span.crid + + +def test_validate_attributes(base_span: BaseSpan) -> None: + attributes = { + "field1": 1, + "field2": "two", + } + filtered_attributes = base_span._validate_attributes(attributes) + + assert isinstance(filtered_attributes, dict) + assert len(attributes) == len(filtered_attributes) + for key, value in attributes.items(): + assert key in filtered_attributes.keys() + assert value in filtered_attributes.values() + + +def test_validate_attribute_with_invalid_key_type(base_span: BaseSpan) -> None: + key = 1 + value = "one" + + (validated_key, validated_value) = base_span._validate_attribute(key, value) + + assert not validated_key + assert not validated_value + + +def test_validate_attribute_exception(span: InstanaSpan) -> None: + base_span = BaseSpan(span, None, "test") + key = "field1" + value = span + + with patch( + "instana.span.BaseSpan._convert_attribute_value", + side_effect=Exception("mocked error"), + ): + (validated_key, validated_value) = base_span._validate_attribute(key, value) + assert key == validated_key + assert not validated_value + + +def test_convert_attribute_value(span: InstanaSpan) -> None: + base_span = BaseSpan(span, None, "test") + value = span + + converted_value = base_span._convert_attribute_value(value) + assert " None: + mock = Mock() + mock.__repr__ = Mock(side_effect=Exception("mocked error")) + + converted_value = base_span._convert_attribute_value(mock) + assert not converted_value diff --git a/tests/test_span_registered.py b/tests/test_span_registered.py new file mode 100644 index 00000000..c184d940 --- /dev/null +++ b/tests/test_span_registered.py @@ -0,0 +1,408 @@ +# (c) Copyright IBM Corp. 2024 + +import time +from typing import Any, Dict, Tuple + +import pytest + +from instana.span import InstanaSpan, RegisteredSpan +from instana.span_context import SpanContext + + +@pytest.mark.parametrize( + "span_name, expected_result, attributes", + [ + ("wsgi", ("wsgi", 1, "http"), {}), + ("rabbitmq", ("rabbitmq", 1, "rabbitmq"), {}), + ("gcps-producer", ("gcps", 2, "gcps"), {}), + ("urllib3", ("urllib3", 2, "http"), {}), + ("rabbitmq", ("rabbitmq", 2, "rabbitmq"), {"sort": "publish"}), + ("render", ("render", 3, "render"), {"arguments": "--quiet"}), + ], +) +def test_registered_span( + span_context: SpanContext, + span_name: str, + expected_result: Tuple[str, int, str], + attributes: Dict[str, Any] +) -> None: + service_name = "test-registered-service" + span = InstanaSpan(span_name, span_context, attributes=attributes) + reg_span = RegisteredSpan(span, None, service_name) + + assert expected_result[0] == reg_span.n + assert expected_result[1] == reg_span.k + assert service_name == reg_span.data["service"] + assert expected_result[2] in reg_span.data.keys() + + +def test_collect_http_attributes_with_attributes(span_context: SpanContext) -> None: + span_name = "test-registered-span" + attributes = { + "span.kind": "entry", + "http.host": "localhost", + "http.url": "https://www.instana.com", + "http.header.test": "one more test", + } + service_name = "test-registered-service" + span = InstanaSpan(span_name, span_context, attributes=attributes) + reg_span = RegisteredSpan(span, None, service_name) + + excepted_result = { + "http.host": attributes["http.host"], + "http.url": attributes["http.url"], + "http.header.test": attributes["http.header.test"], + } + + reg_span._collect_http_attributes(span) + + assert excepted_result["http.host"] == reg_span.data["http"]["host"] + assert excepted_result["http.url"] == reg_span.data["http"]["url"] + assert excepted_result["http.header.test"] == reg_span.data["http"]["header"]["test"] + + +def test_populate_local_span_data_with_other_name(span_context: SpanContext, caplog) -> None: + # span_name = "test-registered-span" + # service_name = "test-registered-service" + # span = InstanaSpan(span_name, span_context) + # reg_span = RegisteredSpan(span, None, service_name) + + # expected_msg = f"SpanRecorder: Unknown local span: {span_name}" + + # reg_span._populate_local_span_data(span) + + # assert expected_msg == caplog.record_tuples[0][2] + pass + + +@pytest.mark.parametrize( + "span_name, service_name, attributes", + [ + ( + "aws.lambda.entry", + "lambda", + { + "lambda.arn": "test", + "lambda.trigger": None, + }, + ), + ( + "celery-worker", + "celery", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "gcps-consumer", + "gcps", + { + "gcps.op": "consume", + "gcps.projid": "MY_PROJECT", + "gcps.sub": "MY_SUBSCRIPTION_NAME", + }, + ), + ( + "rpc-server", + "rpc", + { + "rpc.flavor": "Vanilla", + "rpc.host": "localhost", + "rpc.port": 1234, + }, + ), + ], +) +def test_populate_entry_span_data( + span_context: SpanContext, + span_name: str, + service_name: str, + attributes: Dict[str, Any] +) -> None: + span = InstanaSpan(span_name, span_context) + reg_span = RegisteredSpan(span, None, service_name) + + expected_result = {} + for attr, value in attributes.items(): + attrl = attr.split(".") + attrl = attrl[1] if len(attrl) > 1 else attrl[0] + expected_result[attrl] = value + + span.set_attributes(attributes) + reg_span._populate_entry_span_data(span) + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + +@pytest.mark.parametrize( + "attributes", + [ + { + "lambda.arn": "test", + "lambda.trigger": "aws:api.gateway", + "http.host": "localhost", + "http.url": "https://www.instana.com", + + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:cloudwatch.events", + "lambda.cw.events.resources": "Resource 1", + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:cloudwatch.logs", + "lambda.cw.logs.group": "My Group", + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:s3", + "lambda.s3.events": "Event 1", + }, + { + "lambda.arn": "test", + "lambda.trigger": "aws:sqs", + "lambda.sqs.messages": "Message 1", + }, + ], +) +def test_populate_entry_span_data_AWSlambda( + span_context: SpanContext, + attributes: Dict[str, Any] +) -> None: + span_name = "aws.lambda.entry" + service_name = "lambda" + expected_result = attributes.copy() + + span = InstanaSpan(span_name, span_context) + reg_span = RegisteredSpan(span, None, service_name) + + span.set_attributes(attributes) + reg_span._populate_entry_span_data(span) + + assert "python" == reg_span.data["lambda"]["runtime"] + assert "Unknown" == reg_span.data["lambda"]["functionName"] + assert "test" == reg_span.data["lambda"]["arn"] + assert expected_result["lambda.trigger"] == reg_span.data["lambda"]["trigger"] + + if expected_result["lambda.trigger"] == "aws:api.gateway": + assert expected_result["http.host"] == reg_span.data["http"]["host"] + assert expected_result["http.url"] == reg_span.data["http"]["url"] + + elif expected_result["lambda.trigger"] == "aws:cloudwatch.events": + assert expected_result["lambda.cw.events.resources"] == reg_span.data["lambda"]["cw"]["events"]["resources"] + elif expected_result["lambda.trigger"] == "aws:cloudwatch.logs": + assert expected_result["lambda.cw.logs.group"] == reg_span.data["lambda"]["cw"]["logs"]["group"] + elif expected_result["lambda.trigger"] == "aws:s3": + assert expected_result["lambda.s3.events"] == reg_span.data["lambda"]["s3"]["events"] + elif expected_result["lambda.trigger"] == "aws:sqs": + assert expected_result["lambda.sqs.messages"] == reg_span.data["lambda"]["sqs"]["messages"] + +@pytest.mark.parametrize( + "span_name, service_name, attributes", + [ + ( + "cassandra", + "cassandra", + { + "cassandra.cluster": "my_cluster", + "cassandra.error": "minor error", + }, + ), + ( + "celery-client", + "celery", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "couchbase", + "couchbase", + { + "couchbase.hostname": "localhost", + "couchbase.error_type": 1234, + }, + ), + ( + "rabbitmq", + "rabbitmq", + { + "address": "localhost", + "key": 1234, + }, + ), + ( + "redis", + "redis", + { + "command": "ls -l", + "redis.error": "minor error", + }, + ), + ( + "rpc-client", + "rpc", + { + "rpc.flavor": "Vanilla", + "rpc.host": "localhost", + "rpc.port": 1234, + }, + ), + ( + "sqlalchemy", + "sqlalchemy", + { + "sqlalchemy.sql": "SELECT * FROM everything;", + "sqlalchemy.err": "Impossible select everything from everything!", + }, + ), + ( + "mysql", + "mysql", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "postgres", + "pg", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "mongo", + "mongo", + { + "command": "IDK", + "error": "minor error", + }, + ), + ( + "gcs", + "gcs", + { + "gcs.op": "produce", + "gcs.projectId": "MY_PROJECT", + "gcs.accessId": "Can not tell you!", + }, + ), + ( + "gcps-producer", + "gcps", + { + "gcps.op": "produce", + "gcps.projid": "MY_PROJECT", + "gcps.top": "MY_SUBSCRIPTION_NAME", + }, + ), + ], +) +def test_populate_exit_span_data( + span_context: SpanContext, + span_name: str, + service_name: str, + attributes: Dict[str, Any] +) -> None: + span = InstanaSpan(span_name, span_context) + reg_span = RegisteredSpan(span, None, service_name) + + expected_result = {} + for attr, value in attributes.items(): + attrl = attr.split(".") + attrl = attrl[1] if len(attrl) > 1 else attrl[0] + expected_result[attrl] = value + + span.set_attributes(attributes) + reg_span._populate_exit_span_data(span) + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + +@pytest.mark.parametrize( + "attributes", + [ + { + "op": "test", + "http.host": "localhost", + "http.url": "https://www.instana.com", + }, + { + "payload": { + "blah": "bleh", + "blih": "bloh", + }, + "http.host": "localhost", + "http.url": "https://www.instana.com", + }, + ], +) +def test_populate_exit_span_data_boto3( + span_context: SpanContext, + attributes: Dict[str, Any] +) -> None: + span_name = service_name = "boto3" + expected_result = attributes.copy() + + + span = InstanaSpan(span_name, span_context) + reg_span = RegisteredSpan(span, None, service_name) + + # expected_result = {} + # for attr, value in attributes.items(): + # attrl = attr.split(".") + # attrl = attrl[1] if len(attrl) > 1 else attrl[0] + # expected_result[attrl] = value + + span.set_attributes(attributes) + reg_span._populate_exit_span_data(span) + + assert expected_result.pop("http.host", None) == reg_span.data["http"]["host"] + assert expected_result.pop("http.url", None) == reg_span.data["http"]["url"] + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + + +def test_populate_exit_span_data_log(span_context: SpanContext) -> None: + span_name = service_name = "log" + span = InstanaSpan(span_name, span_context) + reg_span = RegisteredSpan(span, None, service_name) + + excepted_text = "Houston, we have a problem!" + events = [ + ( + "test_populate_exit_span_data_log_event_with_message", + { + "field1": 1, + "field2": "two", + "message": excepted_text, + }, + time.time_ns(), + ), + ( + "test_populate_exit_span_data_log_event_with_parameters", + { + "field1": 1, + "field2": "two", + "parameters": excepted_text, + }, + time.time_ns(), + ), + ] + + for (event_name, attributes, timestamp) in events: + span.add_event(event_name, attributes, timestamp) + + reg_span._populate_exit_span_data(span) + + assert excepted_text == reg_span.data["event"]["message"] + assert excepted_text == reg_span.data["event"]["parameters"] diff --git a/tests/test_span_sdk.py b/tests/test_span_sdk.py new file mode 100644 index 00000000..c7fc8d98 --- /dev/null +++ b/tests/test_span_sdk.py @@ -0,0 +1,83 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import Tuple + +import pytest + +from instana.span import InstanaSpan, SDKSpan +from instana.span_context import SpanContext + + +def test_sdkspan(span_context: SpanContext) -> None: + span_name = "test-sdk-span" + service_name = "test-sdk" + attributes = { + "span.kind": "entry", + "arguments": "--quiet", + "return": "True", + } + span = InstanaSpan(span_name, span_context, attributes=attributes) + sdk_span = SDKSpan(span, None, service_name) + + expected_result = { + "n": "sdk", + "k": 1, + "data": { + "service": service_name, + "sdk": { + "name": span_name, + "type": attributes["span.kind"], + "custom": { + "attributes": attributes, + }, + "arguments": attributes["arguments"], + "return": attributes["return"], + }, + }, + } + + assert expected_result["n"] == sdk_span.n + assert expected_result["k"] == sdk_span.k + assert len(expected_result["data"]) == len(sdk_span.data) + assert expected_result["data"]["service"] == sdk_span.data["service"] + assert len(expected_result["data"]["sdk"]) == len(sdk_span.data["sdk"]) + assert expected_result["data"]["sdk"]["name"] == sdk_span.data["sdk"]["name"] + assert expected_result["data"]["sdk"]["type"] == sdk_span.data["sdk"]["type"] + assert len(attributes) == len(sdk_span.data["sdk"]["custom"]["attributes"]) + assert attributes == sdk_span.data["sdk"]["custom"]["attributes"] + assert attributes["arguments"] == sdk_span.data["sdk"]["arguments"] + assert attributes["return"] == sdk_span.data["sdk"]["return"] + + +@pytest.mark.parametrize( + "span_kind, expected_result", + [ + (None, ("intermediate", 3)), + ("entry", ("entry", 1)), + ("server", ("entry", 1)), + ("consumer", ("entry", 1)), + ("exit", ("exit", 2)), + ("client", ("exit", 2)), + ("producer", ("exit", 2)), + ], +) +def test_sdkspan_get_span_kind( + span_context: SpanContext, + span_kind: str, + expected_result: Tuple[str, int], +) -> None: + attributes = { + "span.kind": span_kind, + } + span = InstanaSpan("test-sdk-span", span_context, attributes=attributes) + sdk_span = SDKSpan(span, None, "test") + + kind = sdk_span.get_span_kind(span) + + assert expected_result == kind + + +def test_sdkspan_get_span_kind_with_no_attributes(span: InstanaSpan) -> None: + sdk_span = SDKSpan(span, None, "test") + kind = sdk_span.get_span_kind(span) + assert ("intermediate", 3) == kind From 62e6301e560f0ae3fca76403b9e775a4afd64f81 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 4 Jul 2024 17:02:39 +0530 Subject: [PATCH 022/172] adapt get_current_span(), get_active_tracer() & get_tracer_tuple() to OTel Signed-off-by: Varsha GS --- src/instana/util/traceutils.py | 25 +++++++++++++++---------- tests/conftest.py | 7 +++++++ tests/test_span.py | 8 ++++---- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index a5b33304..89d3c3be 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -1,8 +1,12 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -from ..singletons import agent, tracer, async_tracer, tornado_tracer -from ..log import logger +from typing import Optional, Tuple + +from instana.log import logger +from instana.singletons import agent, tracer, async_tracer, tornado_tracer +from instana.span import InstanaSpan, get_current_span +from instana.tracer import InstanaTracer def extract_custom_headers(tracing_span, headers): @@ -16,14 +20,12 @@ def extract_custom_headers(tracing_span, headers): logger.debug("extract_custom_headers: ", exc_info=True) -def get_active_tracer(): +def get_active_tracer() -> Optional[InstanaTracer]: try: - if tracer.active_span: + # ToDo: Might have to add additional stuff when testing with async and tornado tracer + current_span = get_current_span() + if current_span and current_span.is_recording(): return tracer - elif async_tracer.active_span: - return async_tracer - elif tornado_tracer.active_span: - return tornado_tracer else: return None except Exception: @@ -32,10 +34,13 @@ def get_active_tracer(): return None -def get_tracer_tuple(): +def get_tracer_tuple() -> ( + Tuple[Optional[InstanaTracer], Optional[InstanaSpan], Optional[str]] +): active_tracer = get_active_tracer() + current_span = get_current_span() if active_tracer: - return (active_tracer, active_tracer.active_span, active_tracer.active_span.operation_name) + return (active_tracer, current_span, current_span.name) elif agent.options.allow_exit_as_root: return (tracer, None, None) return (None, None, None) diff --git a/tests/conftest.py b/tests/conftest.py index 901be6ca..540ce81c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,8 @@ from instana.span import BaseSpan, InstanaSpan # noqa: E402 from instana.span_context import SpanContext # noqa: E402 +from opentelemetry.trace import set_span_in_context +from opentelemetry.context.context import Context collect_ignore_glob = [ "*autoprofile*", @@ -128,3 +130,8 @@ def span(span_context: SpanContext) -> InstanaSpan: @pytest.fixture def base_span(span: InstanaSpan) -> BaseSpan: return BaseSpan(span, None, "test") + + +@pytest.fixture +def context(span: InstanaSpan) -> Context: + return set_span_in_context(span) diff --git a/tests/test_span.py b/tests/test_span.py index 5f30f9f1..bda0333b 100644 --- a/tests/test_span.py +++ b/tests/test_span.py @@ -44,6 +44,7 @@ def test_span_get_span_context( trace_id: int, span_id: int, ) -> None: + span_name = "test-span" span = InstanaSpan(span_name, span_context) @@ -714,10 +715,9 @@ def test_span_assure_errored_exception(span_context: SpanContext) -> None: assert not span.attributes -def test_get_current_span(span_context) -> None: - # span = get_current_span(span_context) - # assert span - pass +def test_get_current_span(context) -> None: + span = get_current_span(context) + assert isinstance(span, InstanaSpan) def test_get_current_span_INVALID_SPAN() -> None: From b114037f776143ae68d385812b655060938fc07b Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 4 Jul 2024 17:06:23 +0530 Subject: [PATCH 023/172] adapt inject and extract to OTel Co-authored-by: Paulo Vital Signed-off-by: Varsha GS --- src/instana/collector/host.py | 3 +- src/instana/propagators/base_propagator.py | 35 ++++++++++++++++++---- src/instana/tracer.py | 32 ++++++++++++++++++-- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/instana/collector/host.py b/src/instana/collector/host.py index d415dfde..d5e3b77f 100644 --- a/src/instana/collector/host.py +++ b/src/instana/collector/host.py @@ -6,6 +6,7 @@ """ from time import time +from typing import DefaultDict, Any from instana.collector.base import BaseCollector from instana.collector.helpers.runtime import RuntimeHelper @@ -72,7 +73,7 @@ def should_send_snapshot_data(self) -> bool: return True return False - def prepare_payload(self) -> DictionaryOfStan: + def prepare_payload(self) -> DefaultDict[Any, Any]: payload = DictionaryOfStan() payload["spans"] = [] payload["profiles"] = [] diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index 18e379b7..29be8ce4 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -11,6 +11,13 @@ from instana.w3c_trace_context.traceparent import Traceparent from instana.w3c_trace_context.tracestate import Tracestate +from opentelemetry.trace import ( + INVALID_SPAN_ID, + INVALID_TRACE_ID, + NonRecordingSpan, + set_span_in_context, +) +from opentelemetry.context.context import Context # The carrier can be a dict or a list. # Using the trace header as an example, it can be in the following forms @@ -154,7 +161,7 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar correlation = False disable_traceparent = os.environ.get("INSTANA_DISABLE_W3C_TRACE_CORRELATION", "") instana_ancestor = None - ctx = SpanContext() + ctx = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) if level and "correlationType" in level: trace_id, span_id = [None] * 2 correlation = True @@ -166,7 +173,12 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar ctx.correlation_type = None ctx.correlation_id = None - if trace_id and span_id: + if ( + trace_id + and span_id + and trace_id != INVALID_TRACE_ID + and span_id != INVALID_SPAN_ID + ): ctx.trace_id = trace_id[-16:] # only the last 16 chars ctx.span_id = span_id[-16:] # only the last 16 chars ctx.synthetic = synthetic is not None @@ -290,9 +302,22 @@ def extract(self, carrier, disable_w3c_trace_context=False): if traceparent: traceparent = self._tp.validate(traceparent) - ctx = self.__determine_span_context(trace_id, span_id, level, synthetic, traceparent, tracestate, - disable_w3c_trace_context) - + if trace_id is None: + trace_id = INVALID_TRACE_ID + if span_id is None: + span_id = INVALID_SPAN_ID + + span_context = self.__determine_span_context( + trace_id, + span_id, + level, + synthetic, + traceparent, + tracestate, + disable_w3c_trace_context, + ) + ctx = set_span_in_context(NonRecordingSpan(span_context), Context()) return ctx + except Exception: logger.debug("extract error:", exc_info=True) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index ad89cc3b..b8454021 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -23,7 +23,9 @@ from instana.agent.host import HostAgent from instana.agent.test import TestAgent from instana.log import logger +from instana.propagators.base_propagator import CarrierT from instana.propagators.binary_propagator import BinaryPropagator +from instana.propagators.exceptions import UnsupportedFormatException from instana.propagators.format import Format from instana.propagators.http_propagator import HTTPPropagator from instana.propagators.text_propagator import TextPropagator @@ -86,8 +88,9 @@ def __init__( sampler: Sampler, recorder: StanRecorder, span_processor: Union[HostAgent, TestAgent], - propagators: - Mapping[str, Union[BinaryPropagator, HTTPPropagator, TextPropagator]], + propagators: Mapping[ + str, Union[BinaryPropagator, HTTPPropagator, TextPropagator] + ], ) -> None: self._tracer_id = generate_id() self._sampler = sampler @@ -239,6 +242,31 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: return span_context + def inject( + self, + span_context: SpanContext, + format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], + carrier: CarrierT, + disable_w3c_trace_context: bool = False, + ) -> Optional[CarrierT]: + if format in self._propagators: + return self._propagators[format].inject( + span_context, carrier, disable_w3c_trace_context + ) + + raise UnsupportedFormatException() + + def extract( + self, + format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], + carrier: CarrierT, + disable_w3c_trace_context: bool = False, + ) -> Optional[Context]: + if format in self._propagators: + return self._propagators[format].extract(carrier, disable_w3c_trace_context) + + raise UnsupportedFormatException() + # Used by __add_stack re_tracer_frame = re.compile(r"/instana/.*\.py$") From 4c29b8cf937cf4a90d0c139589c284c3e6620ec8 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 4 Jul 2024 17:07:35 +0530 Subject: [PATCH 024/172] adapt traceparent, tracestate to OTel Co-authored-by: Paulo Vital Signed-off-by: Varsha GS --- src/instana/w3c_trace_context/traceparent.py | 24 ++++++++++++++------ src/instana/w3c_trace_context/tracestate.py | 6 +++-- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/instana/w3c_trace_context/traceparent.py b/src/instana/w3c_trace_context/traceparent.py index 3175c6cf..bc39fd3a 100644 --- a/src/instana/w3c_trace_context/traceparent.py +++ b/src/instana/w3c_trace_context/traceparent.py @@ -3,6 +3,7 @@ from ..log import logger import re +from typing import Optional # See https://www.w3.org/TR/trace-context-2/#trace-flags for details on the bitmasks. SAMPLED_BITMASK = 0b1; @@ -45,7 +46,13 @@ def get_traceparent_fields(traceparent): logger.debug("Parsing the traceparent failed: {}".format(err)) return None, None, None, None - def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): + def update_traceparent( + self, + traceparent: Optional[str], + in_trace_id: int, + in_span_id: int, + level: int, + ) -> str: """ This method updates the traceparent header or generates one if there was no traceparent incoming header or it was invalid @@ -56,7 +63,11 @@ def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): :return: the updated traceparent header """ if traceparent is None: # modify the trace_id part only when it was not present at all - trace_id = in_trace_id.zfill(32) + trace_id = ( + in_trace_id.zfill(32) + if not isinstance(in_trace_id, int) + else in_trace_id + ) else: # - We do not need the incoming upstream parent span ID for the header we sent downstream. # - We also do not care about the incoming version: The version field we sent downstream needs to match the @@ -67,12 +78,11 @@ def update_traceparent(self, traceparent, in_trace_id, in_span_id, level): # downstream. _, trace_id, _, _ = self.get_traceparent_fields(traceparent) - parent_id = in_span_id.zfill(16) + parent_id = ( + in_span_id.zfill(16) if not isinstance(in_span_id, int) else in_span_id + ) flags = level & SAMPLED_BITMASK flags = format(flags, '0>2x') - traceparent = "{version}-{traceid}-{parentid}-{flags}".format(version=self.SPECIFICATION_VERSION, - traceid=trace_id, - parentid=parent_id, - flags=flags) + traceparent = f"{self.SPECIFICATION_VERSION}-{trace_id}-{parent_id}-{flags}" return traceparent diff --git a/src/instana/w3c_trace_context/tracestate.py b/src/instana/w3c_trace_context/tracestate.py index b1d066ea..f6eb32cb 100644 --- a/src/instana/w3c_trace_context/tracestate.py +++ b/src/instana/w3c_trace_context/tracestate.py @@ -42,8 +42,10 @@ def update_tracestate(self, tracestate, in_trace_id, in_span_id): :return: tracestate updated """ try: - span_id = in_span_id.zfill(16) # if span_id is shorter than 16 characters we prepend zeros - instana_tracestate = "in={};{}".format(in_trace_id, span_id) + span_id = ( + in_span_id.zfill(16) if not isinstance(in_span_id, int) else in_span_id + ) + instana_tracestate = f"in={in_trace_id};{span_id}" if tracestate is None or tracestate == "": tracestate = instana_tracestate else: From f254a6d024ce53a6c00d16811317313e4e08e04c Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 4 Jul 2024 15:47:38 +0200 Subject: [PATCH 025/172] feat: Add CarrierT type to BasePropagator. Signed-off-by: Paulo Vital --- src/instana/propagators/base_propagator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index 29be8ce4..056ba80b 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -2,8 +2,8 @@ # (c) Copyright Instana Inc. 2020 -import sys import os +import typing from instana.log import logger from instana.util.ids import header_to_id, header_to_long_id @@ -19,7 +19,7 @@ ) from opentelemetry.context.context import Context -# The carrier can be a dict or a list. +# The carrier, typed here as CarrierT, can be a dict, a list, or a tuple. # Using the trace header as an example, it can be in the following forms # for extraction: # X-Instana-T @@ -30,6 +30,7 @@ # # For injection, we only support the standard format: # X-Instana-T +CarrierT = typing.TypeVar("CarrierT", typing.Dict, typing.List, typing.Tuple) class BasePropagator(object): From b67edce11710f83537e9fa6203463e7c513a6f69 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 4 Jul 2024 15:54:27 +0200 Subject: [PATCH 026/172] fix: Import of gc in collector/helpers/runtime.py Signed-off-by: Paulo Vital --- src/instana/collector/helpers/runtime.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/instana/collector/helpers/runtime.py b/src/instana/collector/helpers/runtime.py index 2ea68642..8aef48e3 100644 --- a/src/instana/collector/helpers/runtime.py +++ b/src/instana/collector/helpers/runtime.py @@ -2,6 +2,7 @@ # (c) Copyright Instana Inc. 2020 """ Collection helper for the Python runtime """ +import gc import importlib.metadata import os import platform From 8e7891ca3c0e183ed5050686e36d8e61140b02cb Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 4 Jul 2024 16:28:04 +0200 Subject: [PATCH 027/172] fix(tests): Add pytest-mock as test requirement. Signed-off-by: Paulo Vital --- pytest.ini | 1 + tests/conftest.py | 5 ++--- tests/requirements-310.txt | 1 + tests/requirements-312.txt | 1 + tests/requirements-313.txt | 1 + tests/requirements.txt | 1 + tests/test_tracer.py | 1 - 7 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pytest.ini b/pytest.ini index 52835b1d..be615810 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,3 +3,4 @@ log_cli = 1 log_cli_level = WARN log_cli_format = %(asctime)s %(levelname)s %(message)s log_cli_date_format = %H:%M:%S +pythonpath = src diff --git a/tests/conftest.py b/tests/conftest.py index 540ce81c..89d966c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,8 @@ import sys import pytest +from opentelemetry.context.context import Context +from opentelemetry.trace import set_span_in_context if importlib.util.find_spec("celery"): pytest_plugins = ("celery.contrib.pytest",) @@ -20,9 +22,6 @@ from instana.span import BaseSpan, InstanaSpan # noqa: E402 from instana.span_context import SpanContext # noqa: E402 -from opentelemetry.trace import set_span_in_context -from opentelemetry.context.context import Context - collect_ignore_glob = [ "*autoprofile*", "*clients*", diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 22514153..61bcb26a 100644 --- a/tests/requirements-310.txt +++ b/tests/requirements-310.txt @@ -29,6 +29,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock diff --git a/tests/requirements-312.txt b/tests/requirements-312.txt index 8e8aeb34..77015129 100644 --- a/tests/requirements-312.txt +++ b/tests/requirements-312.txt @@ -27,6 +27,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index 44261b13..b0f9588a 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -34,6 +34,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock diff --git a/tests/requirements.txt b/tests/requirements.txt index 2310401c..d1f4a5d9 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -28,6 +28,7 @@ protobuf<4.0.0 pymongo>=3.11.4 pyramid>=2.0.1 pytest>=6.2.4 +pytest-mock>=3.12.0 pytz>=2024.1 redis>=3.5.3 requests-mock diff --git a/tests/test_tracer.py b/tests/test_tracer.py index b7fb558f..feae19cc 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,6 +1,5 @@ # (c) Copyright IBM Corp. 2024 -from unittest.mock import patch from opentelemetry.trace import set_span_in_context from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID import pytest From 696cf7c2887b7d3a48aa0be134f1ab608450232e Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 4 Jul 2024 16:38:40 +0200 Subject: [PATCH 028/172] ci(OTel): Remove Cassandra, Couchbase and Gevent tests Signed-off-by: Paulo Vital --- .circleci/config.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 41e885cb..98d2e421 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -364,9 +364,9 @@ workflows: - python311 - python312 - python313 - - py39cassandra - - py39couchbase - - py39gevent_starlette + # - py39cassandra + # - py39couchbase + # - py39gevent_starlette - py311googlecloud - py312googlecloud - final_job: @@ -377,8 +377,8 @@ workflows: - python311 - python312 - python313 - - py39cassandra - - py39couchbase - - py39gevent_starlette + # - py39cassandra + # - py39couchbase + # - py39gevent_starlette - py311googlecloud - py312googlecloud From c937dae60b852fb21885c94388b94fa2e3363c46 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 9 Jul 2024 09:42:14 +0200 Subject: [PATCH 029/172] fix: Record Spans during closing. Adapt the code to record the Spans during the closing execution. Signed-off-by: Paulo Vital --- src/instana/recorder.py | 18 ++++++++++-------- src/instana/singletons.py | 7 +++---- src/instana/span.py | 27 ++++++++++++++++----------- src/instana/tracer.py | 27 ++++++++++++++++----------- 4 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/instana/recorder.py b/src/instana/recorder.py index 74926705..bcf3d50b 100644 --- a/src/instana/recorder.py +++ b/src/instana/recorder.py @@ -5,8 +5,10 @@ import os import queue +from typing import List, Optional -from .span import RegisteredSpan, SDKSpan +from instana.agent.base import BaseAgent +from instana.span import InstanaSpan, RegisteredSpan, SDKSpan class StanRecorder(object): @@ -47,21 +49,21 @@ class StanRecorder(object): # Recorder thread for collection/reporting of spans thread = None - def __init__(self, agent=None): + def __init__(self, agent: Optional[BaseAgent] = None) -> None: if agent is None: # Late import to avoid circular import # pylint: disable=import-outside-toplevel - from .singletons import get_agent + from instana.singletons import get_agent self.agent = get_agent() else: self.agent = agent - def queue_size(self): + def queue_size(self) -> int: """Return the size of the queue; how may spans are queued,""" return self.agent.collector.span_queue.qsize() - def queued_spans(self): + def queued_spans(self) -> List[InstanaSpan]: """Get all of the spans in the queue""" span = None spans = [] @@ -89,9 +91,9 @@ def clear_spans(self): if not self.agent.collector.span_queue.empty(): self.queued_spans() - def record_span(self, span): + def record_span(self, span: InstanaSpan) -> None: """ - Convert the passed BasicSpan into and add it to the span queue + Convert the passed Span into JSON and add it to the span queue """ if span.context.suppression: return @@ -102,7 +104,7 @@ def record_span(self, span): if "INSTANA_SERVICE_NAME" in os.environ: service_name = self.agent.options.service_name - if span.operation_name in self.REGISTERED_SPANS: + if span.name in self.REGISTERED_SPANS: json_span = RegisteredSpan(span, source, service_name) else: service_name = self.agent.options.service_name diff --git a/src/instana/singletons.py b/src/instana/singletons.py index de605439..0b22e903 100644 --- a/src/instana/singletons.py +++ b/src/instana/singletons.py @@ -5,8 +5,8 @@ from opentelemetry import trace -from .autoprofile.profiler import Profiler -from .tracer import InstanaTracerProvider +from instana.autoprofile.profiler import Profiler +from instana.tracer import InstanaTracerProvider agent = None tracer = None @@ -97,8 +97,7 @@ def set_agent(new_agent): # The global OpenTelemetry compatible tracer used internally by # this package. -provider = InstanaTracerProvider(recorder=span_recorder) -provider.add_span_processor(agent) +provider = InstanaTracerProvider(span_processor=span_recorder, exporter=agent) # Sets the global default tracer provider trace.set_tracer_provider(provider) diff --git a/src/instana/span.py b/src/instana/span.py index 21d9d305..554035f3 100644 --- a/src/instana/span.py +++ b/src/instana/span.py @@ -13,28 +13,29 @@ - SDKSpan: Class that represents an SDK type span - RegisteredSpan: Class that represents a Registered type span """ -import six -from typing import Dict, Optional, Union, Sequence, Tuple from threading import Lock from time import time_ns +from typing import Dict, Optional, Sequence, Tuple, Union +import six +from opentelemetry.context import get_value +from opentelemetry.context.context import Context from opentelemetry.trace import ( - Span, + _SPAN_KEY, DEFAULT_TRACE_OPTIONS, DEFAULT_TRACE_STATE, INVALID_SPAN_ID, INVALID_TRACE_ID, - _SPAN_KEY, + Span, ) -from opentelemetry.util import types -from opentelemetry.trace.status import Status, StatusCode from opentelemetry.trace.span import NonRecordingSpan -from opentelemetry.context import get_value -from opentelemetry.context.context import Context +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util import types -from .span_context import SpanContext -from .log import logger -from .util import DictionaryOfStan +from instana.log import logger +from instana.recorder import StanRecorder +from instana.span_context import SpanContext +from instana.util import DictionaryOfStan class Event: @@ -72,6 +73,7 @@ def __init__( self, name: str, context: SpanContext, + span_processor: StanRecorder, parent_id: Optional[str] = None, start_time: Optional[int] = None, end_time: Optional[int] = None, @@ -81,6 +83,7 @@ def __init__( ) -> None: self._name = name self._context = context + self._span_processor = span_processor self._lock = Lock() self._start_time = start_time or time_ns() self._end_time = end_time @@ -245,6 +248,8 @@ def end(self, end_time: Optional[int] = None) -> None: with self._lock: self._end_time = end_time if end_time is not None else time_ns() self._duration = self._end_time - self._start_time + + self._span_processor.record_span(self) def mark_as_errored(self, attributes: types.Attributes = None) -> None: """ diff --git a/src/instana/tracer.py b/src/instana/tracer.py index b8454021..9d82cc9f 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -40,12 +40,12 @@ class InstanaTracerProvider(TracerProvider): def __init__( self, sampler: Optional[Sampler] = None, - recorder: Optional[StanRecorder] = None, - span_processor: Optional[Union[HostAgent, TestAgent]] = None, + span_processor: Optional[StanRecorder] = None, + exporter: Optional[Union[HostAgent, TestAgent]] = None, ) -> None: self.sampler = sampler or InstanaSampler() - self.recorder = recorder or StanRecorder() - self._span_processor = span_processor or HostAgent() + self._span_processor = span_processor or StanRecorder() + self._exporter = exporter or HostAgent() self._propagators = {} self._propagators[Format.HTTP_HEADERS] = HTTPPropagator() self._propagators[Format.TEXT_MAP] = TextPropagator() @@ -63,14 +63,14 @@ def get_tracer( return InstanaTracer( self.sampler, - self.recorder, + self._exporter, self._span_processor, self._propagators, ) def add_span_processor( self, - span_processor: Union[HostAgent, TestAgent], + span_processor: StanRecorder, ) -> None: """Registers a new SpanProcessor for the TracerProvider.""" self._span_processor = span_processor @@ -86,16 +86,16 @@ class InstanaTracer(Tracer): def __init__( self, sampler: Sampler, - recorder: StanRecorder, - span_processor: Union[HostAgent, TestAgent], + span_processor: StanRecorder, + exporter: Union[HostAgent, TestAgent], propagators: Mapping[ str, Union[BinaryPropagator, HTTPPropagator, TextPropagator] ], ) -> None: self._tracer_id = generate_id() self._sampler = sampler - self._recorder = recorder self._span_processor = span_processor + self._exporter = exporter self._propagators = propagators @property @@ -103,8 +103,12 @@ def tracer_id(self) -> str: return self._tracer_id @property - def recorder(self) -> Optional[StanRecorder]: - return self._recorder + def span_processor(self) -> Optional[StanRecorder]: + return self._span_processor + + @property + def exporter(self) -> Optional[Union[HostAgent, TestAgent]]: + return self._exporter def start_span( self, @@ -130,6 +134,7 @@ def start_span( span = InstanaSpan( name, span_context, + self._span_processor, parent_id=(None if parent_context is None else parent_context.span_id), start_time=(time.time_ns() if start_time is None else start_time), attributes=attributes, From b196b3cbc0a1493db2902119ac28806feed90103 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 9 Jul 2024 16:35:43 +0200 Subject: [PATCH 030/172] refactor: Instana's Span structure. Refactor the Instana's Span structure to improve performance and prevent potential issues. Declouped the BaseSpan, RegisteredSpan, and SDKSpan from the span.py file to prevent possible circular import errors. Moved all sequences with kind or types of spans to the kind.py file. Created the ReadableSpan class to provide read-only access to span attributes and information. Made the InstanaSpan to multiple inherit from OpenTelemetry's API Span and ReadableSpan classes. Signed-off-by: Paulo Vital --- src/instana/agent/host.py | 12 +- src/instana/collector/utils.py | 11 +- src/instana/recorder.py | 60 +- src/instana/span.py | 846 ---------------------------- src/instana/span/__init__.py | 0 src/instana/span/base_span.py | 120 ++++ src/instana/span/kind.py | 56 ++ src/instana/span/readable_span.py | 107 ++++ src/instana/span/registered_span.py | 334 +++++++++++ src/instana/span/sdk_span.py | 62 ++ src/instana/span/span.py | 251 +++++++++ src/instana/tracer.py | 7 +- src/instana/util/traceutils.py | 2 +- 13 files changed, 965 insertions(+), 903 deletions(-) delete mode 100644 src/instana/span.py create mode 100644 src/instana/span/__init__.py create mode 100644 src/instana/span/base_span.py create mode 100644 src/instana/span/kind.py create mode 100644 src/instana/span/readable_span.py create mode 100644 src/instana/span/registered_span.py create mode 100644 src/instana/span/sdk_span.py create mode 100644 src/instana/span/span.py diff --git a/src/instana/agent/host.py b/src/instana/agent/host.py index 89daff8e..9bb3dd8e 100644 --- a/src/instana/agent/host.py +++ b/src/instana/agent/host.py @@ -280,11 +280,13 @@ def report_data_payload(self, payload): self.last_seen = datetime.now() # Report metrics - metric_bundle = payload["metrics"]["plugins"][0]["data"] - response = self.client.post(self.__data_url(), - data=to_json(metric_bundle), - headers={"Content-Type": "application/json"}, - timeout=0.8) + metric_count = len(payload['metrics']) + if metric_count > 0: + metric_bundle = payload["metrics"]["plugins"][0]["data"] + response = self.client.post(self.__data_url(), + data=to_json(metric_bundle), + headers={"Content-Type": "application/json"}, + timeout=0.8) if response is not None and 200 <= response.status_code <= 204: self.last_seen = datetime.now() diff --git a/src/instana/collector/utils.py b/src/instana/collector/utils.py index 4bb4e9ff..a3dbc209 100644 --- a/src/instana/collector/utils.py +++ b/src/instana/collector/utils.py @@ -1,15 +1,16 @@ # (c) Copyright IBM Corp. 2024 -from typing import List +from typing import TYPE_CHECKING, List from opentelemetry.trace.span import format_span_id -from instana.span import InstanaSpan +if TYPE_CHECKING: + from instana.span.span import InstanaSpan def format_trace_and_span_ids( - queued_spans: List[InstanaSpan], -) -> List[InstanaSpan]: + queued_spans: List["InstanaSpan"], +) -> List["InstanaSpan"]: """ Format the Trace, Parent Span, and Span IDs of Spans to be a 64-bit Hexadecimal String instead of Integer before being pushed to a @@ -18,7 +19,7 @@ def format_trace_and_span_ids( spans = [] for span in queued_spans: span.t = format_span_id(span.t) - span.p = format_span_id(span.p) span.s = format_span_id(span.s) + span.p = format_span_id(span.p) if span.p else None spans.append(span) return spans diff --git a/src/instana/recorder.py b/src/instana/recorder.py index bcf3d50b..f14e9a60 100644 --- a/src/instana/recorder.py +++ b/src/instana/recorder.py @@ -5,51 +5,24 @@ import os import queue -from typing import List, Optional +from typing import TYPE_CHECKING, List, Optional, Type -from instana.agent.base import BaseAgent -from instana.span import InstanaSpan, RegisteredSpan, SDKSpan +from instana.span.kind import REGISTERED_SPANS +from instana.span.readable_span import ReadableSpan +from instana.span.registered_span import RegisteredSpan +from instana.span.sdk_span import SDKSpan + +if TYPE_CHECKING: + from instana.agent.base import BaseAgent class StanRecorder(object): - THREAD_NAME = "Instana Span Reporting" - - REGISTERED_SPANS = ( - "aiohttp-client", - "aiohttp-server", - "aws.lambda.entry", - "boto3", - "cassandra", - "celery-client", - "celery-worker", - "couchbase", - "django", - "gcs", - "gcps-producer", - "gcps-consumer", - "log", - "memcache", - "mongo", - "mysql", - "postgres", - "pymongo", - "rabbitmq", - "redis", - "render", - "rpc-client", - "rpc-server", - "sqlalchemy", - "tornado-client", - "tornado-server", - "urllib3", - "wsgi", - "asgi", - ) + THREAD_NAME = "InstanaSpan Recorder" # Recorder thread for collection/reporting of spans thread = None - def __init__(self, agent: Optional[BaseAgent] = None) -> None: + def __init__(self, agent: Optional[Type["BaseAgent"]] = None) -> None: if agent is None: # Late import to avoid circular import # pylint: disable=import-outside-toplevel @@ -63,12 +36,13 @@ def queue_size(self) -> int: """Return the size of the queue; how may spans are queued,""" return self.agent.collector.span_queue.qsize() - def queued_spans(self) -> List[InstanaSpan]: - """Get all of the spans in the queue""" + def queued_spans(self) -> List[ReadableSpan]: + """Get all of the spans in the queue.""" span = None spans = [] import time + from .singletons import env_is_test if env_is_test is True: @@ -87,13 +61,13 @@ def queued_spans(self) -> List[InstanaSpan]: return spans def clear_spans(self): - """Clear the queue of spans""" + """Clear the queue of spans.""" if not self.agent.collector.span_queue.empty(): self.queued_spans() - def record_span(self, span: InstanaSpan) -> None: + def record_span(self, span: ReadableSpan) -> None: """ - Convert the passed Span into JSON and add it to the span queue + Convert the passed span into JSON and add it to the span queue. """ if span.context.suppression: return @@ -104,7 +78,7 @@ def record_span(self, span: InstanaSpan) -> None: if "INSTANA_SERVICE_NAME" in os.environ: service_name = self.agent.options.service_name - if span.name in self.REGISTERED_SPANS: + if span.name in REGISTERED_SPANS: json_span = RegisteredSpan(span, source, service_name) else: service_name = self.agent.options.service_name diff --git a/src/instana/span.py b/src/instana/span.py deleted file mode 100644 index 554035f3..00000000 --- a/src/instana/span.py +++ /dev/null @@ -1,846 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2017 - -""" -This module contains the classes that represents spans. - -InstanaSpan - the OpenTelemetry based span used during tracing - -When an InstanaSpan is finished, it is converted into either an SDKSpan -or RegisteredSpan depending on type. - -BaseSpan: Base class containing the commonalities for the two descendants - - SDKSpan: Class that represents an SDK type span - - RegisteredSpan: Class that represents a Registered type span -""" -from threading import Lock -from time import time_ns -from typing import Dict, Optional, Sequence, Tuple, Union - -import six -from opentelemetry.context import get_value -from opentelemetry.context.context import Context -from opentelemetry.trace import ( - _SPAN_KEY, - DEFAULT_TRACE_OPTIONS, - DEFAULT_TRACE_STATE, - INVALID_SPAN_ID, - INVALID_TRACE_ID, - Span, -) -from opentelemetry.trace.span import NonRecordingSpan -from opentelemetry.trace.status import Status, StatusCode -from opentelemetry.util import types - -from instana.log import logger -from instana.recorder import StanRecorder -from instana.span_context import SpanContext -from instana.util import DictionaryOfStan - - -class Event: - def __init__( - self, - name: str, - attributes: types.Attributes = None, - timestamp: Optional[int] = None, - ) -> None: - self._name = name - self._attributes = attributes - if timestamp is None: - self._timestamp = time_ns() - else: - self._timestamp = timestamp - - @property - def name(self) -> str: - return self._name - - @property - def timestamp(self) -> int: - return self._timestamp - - @property - def attributes(self) -> types.Attributes: - return self._attributes - - -class InstanaSpan(Span): - stack = None - synthetic = False - - def __init__( - self, - name: str, - context: SpanContext, - span_processor: StanRecorder, - parent_id: Optional[str] = None, - start_time: Optional[int] = None, - end_time: Optional[int] = None, - attributes: types.Attributes = {}, - events: Sequence[Event] = [], - status: Optional[Status] = Status(StatusCode.UNSET), - ) -> None: - self._name = name - self._context = context - self._span_processor = span_processor - self._lock = Lock() - self._start_time = start_time or time_ns() - self._end_time = end_time - self._duration = 0 - self._attributes = attributes - self._events = events - self._parent_id = parent_id - self._status = status - - if context.synthetic: - self.synthetic = True - - - @property - def name(self) -> str: - return self._name - - def get_span_context(self) -> SpanContext: - return self._context - - @property - def context(self) -> SpanContext: - return self._context - - @property - def start_time(self) -> Optional[int]: - return self._start_time - - @property - def end_time(self) -> Optional[int]: - return self._end_time - - @property - def duration(self) -> int: - return self._duration - - @property - def attributes(self) -> types.Attributes: - return self._attributes - - def set_attributes(self, attributes: Dict[str, types.AttributeValue]) -> None: - if not self._attributes: - self._attributes = {} - - with self._lock: - for key, value in attributes.items(): - self._attributes[key] = value - - def set_attribute(self, key: str, value: types.AttributeValue) -> None: - return self.set_attributes({key: value}) - - @property - def events(self) -> Sequence[Event]: - return self._events - - @property - def status(self) -> Status: - return self._status - - @property - def parent_id(self) -> int: - return self._parent_id - - def update_name(self, name: str) -> None: - with self._lock: - self._name = name - - def is_recording(self) -> bool: - return self._end_time is None - - def set_status( - self, - status: Union[Status, StatusCode], - description: Optional[str] = None, - ) -> None: - # Ignore future calls if status is already set to OK - # Ignore calls to set to StatusCode.UNSET - if isinstance(status, Status): - if ( - self._status - and self._status.status_code is StatusCode.OK - or status.status_code is StatusCode.UNSET - ): - return - if description is not None: - logger.warning( - "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", - description, - ) - self._status = status - elif isinstance(status, StatusCode): - if ( - self._status - and self._status.status_code is StatusCode.OK - or status is StatusCode.UNSET - ): - return - self._status = Status(status, description) - - def add_event( - self, - name: str, - attributes: types.Attributes = None, - timestamp: Optional[int] = None, - ) -> None: - - event = Event( - name=name, - attributes=attributes, - timestamp=timestamp, - ) - - self._events.append(event) - - def record_exception( - self, - exception: Exception, - attributes: types.Attributes = None, - timestamp: Optional[int] = None, - escaped: bool = False, - ) -> None: - """ - Records an exception as a span event. This will record pertinent info from the exception and - assure that this span is marked as errored. - """ - try: - message = "" - self.mark_as_errored() - if hasattr(exception, "__str__") and len(str(exception)) > 0: - message = str(exception) - elif hasattr(exception, "message") and exception.message is not None: - message = exception.message - else: - message = repr(exception) - - if self.name in ["rpc-server", "rpc-client"]: - self.set_attribute("rpc.error", message) - elif self.name == "mysql": - self.set_attribute("mysql.error", message) - elif self.name == "postgres": - self.set_attribute("pg.error", message) - elif self.name in RegisteredSpan.HTTP_SPANS: - self.set_attribute("http.error", message) - elif self.name in ["celery-client", "celery-worker"]: - self.set_attribute("error", message) - elif self.name == "sqlalchemy": - self.set_attribute("sqlalchemy.err", message) - elif self.name == "aws.lambda.entry": - self.set_attribute("lambda.error", message) - else: - _attributes = {"message": message} - if attributes: - _attributes.update(attributes) - self.add_event( - name="exception", attributes=_attributes, timestamp=timestamp - ) - except Exception: - logger.debug("span.record_exception", exc_info=True) - raise - - def end(self, end_time: Optional[int] = None) -> None: - with self._lock: - self._end_time = end_time if end_time is not None else time_ns() - self._duration = self._end_time - self._start_time - - self._span_processor.record_span(self) - - def mark_as_errored(self, attributes: types.Attributes = None) -> None: - """ - Mark this span as errored. - - @param attributes: optional attributes to add to the span - """ - try: - ec = self.attributes.get("ec", 0) - self.set_attribute("ec", ec + 1) - - if attributes is not None and isinstance(attributes, dict): - for key in attributes: - self.set_attribute(key, attributes[key]) - except Exception: - logger.debug("span.mark_as_errored", exc_info=True) - - def assure_errored(self) -> None: - """ - Make sure that this span is marked as errored. - @return: None - """ - try: - ec = self.attributes.get("ec", None) - if ec is None or ec == 0: - self.set_attribute("ec", 1) - except Exception: - logger.debug("span.assure_errored", exc_info=True) - - -INVALID_SPAN_CONTEXT = SpanContext( - trace_id=INVALID_TRACE_ID, - span_id=INVALID_SPAN_ID, - is_remote=False, - trace_flags=DEFAULT_TRACE_OPTIONS, - trace_state=DEFAULT_TRACE_STATE, -) -INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) - - -def get_current_span(context: Optional[Context] = None) -> InstanaSpan: - """Retrieve the current span. - - Args: - context: A Context object. If one is not passed, the - default current context is used instead. - - Returns: - The Span set in the context if it exists. INVALID_SPAN otherwise. - """ - span = get_value(_SPAN_KEY, context=context) - if span is None or not isinstance(span, InstanaSpan): - return INVALID_SPAN - return span - - -class BaseSpan(object): - sy = None - - def __str__(self) -> str: - return "BaseSpan(%s)" % self.__dict__.__str__() - - def __repr__(self) -> str: - return self.__dict__.__str__() - - def __init__(self, span, source, service_name, **kwargs) -> None: - # pylint: disable=invalid-name - self.t = span.context.trace_id - self.p = span.parent_id - # self.p = span.context.span_id if span.context.is_remote else None - self.s = span.context.span_id - self.ts = round(span.start_time / 10**6) - self.d = round(span.duration / 10**6) - self.f = source - self.ec = span.attributes.pop("ec", None) - self.data = DictionaryOfStan() - self.stack = span.stack - - if span.synthetic is True: - self.sy = span.synthetic - - self.__dict__.update(kwargs) - - def _populate_extra_span_attributes(self, span) -> None: - if span.context.trace_parent: - self.tp = span.context.trace_parent - if span.context.instana_ancestor: - self.ia = span.context.instana_ancestor - if span.context.long_trace_id: - self.lt = span.context.long_trace_id - if span.context.correlation_type: - self.crtp = span.context.correlation_type - if span.context.correlation_id: - self.crid = span.context.correlation_id - - def _validate_attributes(self, attributes): - """ - This method will loop through a set of attributes to validate each key and value. - - :param attributes: dict of attributes - :return: dict - a filtered set of attributes - """ - filtered_attributes = DictionaryOfStan() - for key in attributes.keys(): - validated_key, validated_value = self._validate_attribute( - key, attributes[key] - ) - if validated_key is not None and validated_value is not None: - filtered_attributes[validated_key] = validated_value - return filtered_attributes - - def _validate_attribute(self, key, value): - """ - This method will assure that and are valid to set as a attribute. - If fails the check, an attempt will be made to convert it into - something useful. - - On check failure, this method will return None values indicating that the attribute is - not valid and could not be converted into something useful - - :param key: The attribute key - :param value: The attribute value - :return: Tuple (key, value) - """ - validated_key = None - validated_value = None - - try: - # Attribute keys must be some type of text or string type - if isinstance(key, (six.text_type, six.string_types)): - validated_key = key[0:1024] # Max key length of 1024 characters - - if isinstance( - value, - (bool, float, int, list, dict, six.text_type, six.string_types), - ): - validated_value = value - else: - validated_value = self._convert_attribute_value(value) - else: - logger.debug( - "(non-fatal) attribute names must be strings. attribute discarded for %s", - type(key), - ) - except Exception: - logger.debug("instana.span._validate_attribute: ", exc_info=True) - - return (validated_key, validated_value) - - def _convert_attribute_value(self, value): - final_value = None - - try: - final_value = repr(value) - except Exception: - final_value = ( - "(non-fatal) span.set_attribute: values must be one of these types: bool, float, int, list, " - "set, str or alternatively support 'repr'. attribute discarded" - ) - logger.debug(final_value, exc_info=True) - return None - return final_value - - -class SDKSpan(BaseSpan): - ENTRY_KIND = ["entry", "server", "consumer"] - EXIT_KIND = ["exit", "client", "producer"] - - def __init__(self, span, source, service_name, **kwargs) -> None: - # pylint: disable=invalid-name - super(SDKSpan, self).__init__(span, source, service_name, **kwargs) - - span_kind = self.get_span_kind(span) - - self.n = "sdk" - self.k = span_kind[1] - - if service_name is not None: - self.data["service"] = service_name - - self.data["sdk"]["name"] = span.name - self.data["sdk"]["type"] = span_kind[0] - self.data["sdk"]["custom"]["attributes"] = self._validate_attributes( - span.attributes - ) - - if span.events is not None and len(span.events) > 0: - events = DictionaryOfStan() - for event in span.events: - filtered_attributes = self._validate_attributes(event.attributes) - if len(filtered_attributes.keys()) > 0: - events[repr(event.timestamp)] = filtered_attributes - self.data["sdk"]["custom"]["events"] = events - - if "arguments" in span.attributes: - self.data["sdk"]["arguments"] = span.attributes["arguments"] - - if "return" in span.attributes: - self.data["sdk"]["return"] = span.attributes["return"] - - # if len(span.context.baggage) > 0: - # self.data["baggage"] = span.context.baggage - - def get_span_kind(self, span) -> Tuple[str, int]: - """ - Will retrieve the `span.kind` attribute and return a tuple containing the appropriate string and integer - values for the Instana backend - - :param span: The span to search for the `span.kind` attribute - :return: Tuple (String, Int) - """ - kind = ("intermediate", 3) - if "span.kind" in span.attributes: - if span.attributes["span.kind"] in self.ENTRY_KIND: - kind = ("entry", 1) - elif span.attributes["span.kind"] in self.EXIT_KIND: - kind = ("exit", 2) - return kind - - -class RegisteredSpan(BaseSpan): - HTTP_SPANS = ( - "aiohttp-client", - "aiohttp-server", - "django", - "http", - "tornado-client", - "tornado-server", - "urllib3", - "wsgi", - "asgi", - ) - - EXIT_SPANS = ( - "aiohttp-client", - "boto3", - "cassandra", - "celery-client", - "couchbase", - "log", - "memcache", - "mongo", - "mysql", - "postgres", - "rabbitmq", - "redis", - "rpc-client", - "sqlalchemy", - "tornado-client", - "urllib3", - "pymongo", - "gcs", - "gcps-producer", - ) - - ENTRY_SPANS = ( - "aiohttp-server", - "aws.lambda.entry", - "celery-worker", - "django", - "wsgi", - "rabbitmq", - "rpc-server", - "tornado-server", - "gcps-consumer", - "asgi", - ) - - LOCAL_SPANS = "render" - - def __init__(self, span, source, service_name, **kwargs) -> None: - # pylint: disable=invalid-name - super(RegisteredSpan, self).__init__(span, source, service_name, **kwargs) - self.n = span.name - self.k = 1 - - self.data["service"] = service_name - if span.name in self.ENTRY_SPANS: - # entry - self._populate_entry_span_data(span) - self._populate_extra_span_attributes(span) - elif span.name in self.EXIT_SPANS: - self.k = 2 # exit - self._populate_exit_span_data(span) - elif span.name in self.LOCAL_SPANS: - self.k = 3 # intermediate span - self._populate_local_span_data(span) - - if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": - self.k = 2 # exit - - # unify the span name for gcps-producer and gcps-consumer - if "gcps" in span.name: - self.n = "gcps" - - # Store any leftover attributes in the custom section - if len(span.attributes) > 0: - self.data["custom"]["attributes"] = self._validate_attributes( - span.attributes - ) - - def _populate_entry_span_data(self, span) -> None: - if span.name in self.HTTP_SPANS: - self._collect_http_attributes(span) - - elif span.name == "aws.lambda.entry": - self.data["lambda"]["arn"] = span.attributes.pop("lambda.arn", "Unknown") - self.data["lambda"]["alias"] = None - self.data["lambda"]["runtime"] = "python" - self.data["lambda"]["functionName"] = span.attributes.pop( - "lambda.name", "Unknown" - ) - self.data["lambda"]["functionVersion"] = span.attributes.pop( - "lambda.version", "Unknown" - ) - self.data["lambda"]["trigger"] = span.attributes.pop("lambda.trigger", None) - self.data["lambda"]["error"] = span.attributes.pop("lambda.error", None) - - trigger_type = self.data["lambda"]["trigger"] - - if trigger_type in ["aws:api.gateway", "aws:application.load.balancer"]: - self._collect_http_attributes(span) - elif trigger_type == "aws:cloudwatch.events": - self.data["lambda"]["cw"]["events"]["id"] = span.attributes.pop( - "data.lambda.cw.events.id", None - ) - self.data["lambda"]["cw"]["events"]["more"] = span.attributes.pop( - "lambda.cw.events.more", False - ) - self.data["lambda"]["cw"]["events"]["resources"] = span.attributes.pop( - "lambda.cw.events.resources", None - ) - - elif trigger_type == "aws:cloudwatch.logs": - self.data["lambda"]["cw"]["logs"]["group"] = span.attributes.pop( - "lambda.cw.logs.group", None - ) - self.data["lambda"]["cw"]["logs"]["stream"] = span.attributes.pop( - "lambda.cw.logs.stream", None - ) - self.data["lambda"]["cw"]["logs"]["more"] = span.attributes.pop( - "lambda.cw.logs.more", None - ) - self.data["lambda"]["cw"]["logs"]["events"] = span.attributes.pop( - "lambda.cw.logs.events", None - ) - - elif trigger_type == "aws:s3": - self.data["lambda"]["s3"]["events"] = span.attributes.pop( - "lambda.s3.events", None - ) - elif trigger_type == "aws:sqs": - self.data["lambda"]["sqs"]["messages"] = span.attributes.pop( - "lambda.sqs.messages", None - ) - - elif span.name == "celery-worker": - self.data["celery"]["task"] = span.attributes.pop("task", None) - self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) - self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) - self.data["celery"]["host"] = span.attributes.pop("host", None) - self.data["celery"]["port"] = span.attributes.pop("port", None) - self.data["celery"]["retry-reason"] = span.attributes.pop( - "retry-reason", None - ) - self.data["celery"]["error"] = span.attributes.pop("error", None) - - elif span.name == "gcps-consumer": - self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) - self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) - self.data["gcps"]["sub"] = span.attributes.pop("gcps.sub", None) - - elif span.name == "rabbitmq": - self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) - self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) - self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) - self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) - self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) - - elif span.name == "rpc-server": - self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) - self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) - self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) - self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) - self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) - self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) - # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) - self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) - else: - logger.debug("SpanRecorder: Unknown entry span: %s" % span.name) - - def _populate_local_span_data(self, span) -> None: - if span.name == "render": - self.data["render"]["name"] = span.attributes.pop("name", None) - self.data["render"]["type"] = span.attributes.pop("type", None) - self.data["event"]["message"] = span.attributes.pop("message", None) - self.data["event"]["parameters"] = span.attributes.pop("parameters", None) - else: - logger.debug("SpanRecorder: Unknown local span: %s" % span.name) - - def _populate_exit_span_data(self, span) -> None: - if span.name in self.HTTP_SPANS: - self._collect_http_attributes(span) - - elif span.name == "boto3": - # boto3 also sends http attributes - self._collect_http_attributes(span) - - for attribute in ["op", "ep", "reg", "payload", "error"]: - value = span.attributes.pop(attribute, None) - if value is not None: - if attribute == "payload": - self.data["boto3"][attribute] = self._validate_attributes(value) - else: - self.data["boto3"][attribute] = value - - elif span.name == "cassandra": - self.data["cassandra"]["cluster"] = span.attributes.pop( - "cassandra.cluster", None - ) - self.data["cassandra"]["query"] = span.attributes.pop( - "cassandra.query", None - ) - self.data["cassandra"]["keyspace"] = span.attributes.pop( - "cassandra.keyspace", None - ) - self.data["cassandra"]["fetchSize"] = span.attributes.pop( - "cassandra.fetchSize", None - ) - self.data["cassandra"]["achievedConsistency"] = span.attributes.pop( - "cassandra.achievedConsistency", None - ) - self.data["cassandra"]["triedHosts"] = span.attributes.pop( - "cassandra.triedHosts", None - ) - self.data["cassandra"]["fullyFetched"] = span.attributes.pop( - "cassandra.fullyFetched", None - ) - self.data["cassandra"]["error"] = span.attributes.pop( - "cassandra.error", None - ) - - elif span.name == "celery-client": - self.data["celery"]["task"] = span.attributes.pop("task", None) - self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) - self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) - self.data["celery"]["host"] = span.attributes.pop("host", None) - self.data["celery"]["port"] = span.attributes.pop("port", None) - self.data["celery"]["error"] = span.attributes.pop("error", None) - - elif span.name == "couchbase": - self.data["couchbase"]["hostname"] = span.attributes.pop( - "couchbase.hostname", None - ) - self.data["couchbase"]["bucket"] = span.attributes.pop( - "couchbase.bucket", None - ) - self.data["couchbase"]["type"] = span.attributes.pop("couchbase.type", None) - self.data["couchbase"]["error"] = span.attributes.pop( - "couchbase.error", None - ) - self.data["couchbase"]["error_type"] = span.attributes.pop( - "couchbase.error_type", None - ) - self.data["couchbase"]["sql"] = span.attributes.pop("couchbase.sql", None) - - elif span.name == "rabbitmq": - self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) - self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) - self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) - self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) - self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) - - elif span.name == "redis": - self.data["redis"]["connection"] = span.attributes.pop("connection", None) - self.data["redis"]["driver"] = span.attributes.pop("driver", None) - self.data["redis"]["command"] = span.attributes.pop("command", None) - self.data["redis"]["error"] = span.attributes.pop("redis.error", None) - self.data["redis"]["subCommands"] = span.attributes.pop("subCommands", None) - - elif span.name == "rpc-client": - self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) - self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) - self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) - self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) - self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) - self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) - # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) - self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) - - elif span.name == "sqlalchemy": - self.data["sqlalchemy"]["sql"] = span.attributes.pop("sqlalchemy.sql", None) - self.data["sqlalchemy"]["eng"] = span.attributes.pop("sqlalchemy.eng", None) - self.data["sqlalchemy"]["url"] = span.attributes.pop("sqlalchemy.url", None) - self.data["sqlalchemy"]["err"] = span.attributes.pop("sqlalchemy.err", None) - - elif span.name == "mysql": - self.data["mysql"]["host"] = span.attributes.pop("host", None) - self.data["mysql"]["port"] = span.attributes.pop("port", None) - self.data["mysql"]["db"] = span.attributes.pop("db.instance", None) - self.data["mysql"]["user"] = span.attributes.pop("db.user", None) - self.data["mysql"]["stmt"] = span.attributes.pop("db.statement", None) - self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) - - elif span.name == "postgres": - self.data["pg"]["host"] = span.attributes.pop("host", None) - self.data["pg"]["port"] = span.attributes.pop("port", None) - self.data["pg"]["db"] = span.attributes.pop("db.instance", None) - self.data["pg"]["user"] = span.attributes.pop("db.user", None) - self.data["pg"]["stmt"] = span.attributes.pop("db.statement", None) - self.data["pg"]["error"] = span.attributes.pop("pg.error", None) - - elif span.name == "mongo": - service = "%s:%s" % ( - span.attributes.pop("host", None), - span.attributes.pop("port", None), - ) - namespace = "%s.%s" % ( - span.attributes.pop("db", "?"), - span.attributes.pop("collection", "?"), - ) - - self.data["mongo"]["service"] = service - self.data["mongo"]["namespace"] = namespace - self.data["mongo"]["command"] = span.attributes.pop("command", None) - self.data["mongo"]["filter"] = span.attributes.pop("filter", None) - self.data["mongo"]["json"] = span.attributes.pop("json", None) - self.data["mongo"]["error"] = span.attributes.pop("error", None) - - elif span.name == "gcs": - self.data["gcs"]["op"] = span.attributes.pop("gcs.op", None) - self.data["gcs"]["bucket"] = span.attributes.pop("gcs.bucket", None) - self.data["gcs"]["object"] = span.attributes.pop("gcs.object", None) - self.data["gcs"]["entity"] = span.attributes.pop("gcs.entity", None) - self.data["gcs"]["range"] = span.attributes.pop("gcs.range", None) - self.data["gcs"]["sourceBucket"] = span.attributes.pop( - "gcs.sourceBucket", None - ) - self.data["gcs"]["sourceObject"] = span.attributes.pop( - "gcs.sourceObject", None - ) - self.data["gcs"]["sourceObjects"] = span.attributes.pop( - "gcs.sourceObjects", None - ) - self.data["gcs"]["destinationBucket"] = span.attributes.pop( - "gcs.destinationBucket", None - ) - self.data["gcs"]["destinationObject"] = span.attributes.pop( - "gcs.destinationObject", None - ) - self.data["gcs"]["numberOfOperations"] = span.attributes.pop( - "gcs.numberOfOperations", None - ) - self.data["gcs"]["projectId"] = span.attributes.pop("gcs.projectId", None) - self.data["gcs"]["accessId"] = span.attributes.pop("gcs.accessId", None) - - elif span.name == "gcps-producer": - self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) - self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) - self.data["gcps"]["top"] = span.attributes.pop("gcps.top", None) - - elif span.name == "log": - # use last special key values - for event in span.events: - if "message" in event.attributes: - self.data["event"]["message"] = event.attributes.pop( - "message", None - ) - if "parameters" in event.attributes: - self.data["event"]["parameters"] = event.attributes.pop( - "parameters", None - ) - else: - logger.debug("SpanRecorder: Unknown exit span: %s" % span.name) - - def _collect_http_attributes(self, span) -> None: - self.data["http"]["host"] = span.attributes.pop("http.host", None) - self.data["http"]["url"] = span.attributes.pop("http.url", None) - self.data["http"]["path"] = span.attributes.pop("http.path", None) - self.data["http"]["params"] = span.attributes.pop("http.params", None) - self.data["http"]["method"] = span.attributes.pop("http.method", None) - self.data["http"]["status"] = span.attributes.pop("http.status_code", None) - self.data["http"]["path_tpl"] = span.attributes.pop("http.path_tpl", None) - self.data["http"]["error"] = span.attributes.pop("http.error", None) - - if len(span.attributes) > 0: - custom_headers = [] - for key in span.attributes: - if key[0:12] == "http.header.": - custom_headers.append(key) - - for key in custom_headers: - trimmed_key = key[12:] - self.data["http"]["header"][trimmed_key] = span.attributes.pop(key) diff --git a/src/instana/span/__init__.py b/src/instana/span/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/instana/span/base_span.py b/src/instana/span/base_span.py new file mode 100644 index 00000000..11d2b733 --- /dev/null +++ b/src/instana/span/base_span.py @@ -0,0 +1,120 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import TYPE_CHECKING, Type +import six + +from instana.log import logger +from instana.util import DictionaryOfStan + +if TYPE_CHECKING: + from opentelemetry.trace import Span + + +class BaseSpan(object): + sy = None + + def __str__(self) -> str: + return "BaseSpan(%s)" % self.__dict__.__str__() + + def __repr__(self) -> str: + return self.__dict__.__str__() + + def __init__(self, span: Type["Span"], source, **kwargs) -> None: + # pylint: disable=invalid-name + self.t = span.context.trace_id + self.p = span.parent_id + # self.p = span.context.span_id if span.context.is_remote else None + self.s = span.context.span_id + self.l = span.context.level + self.ts = round(span.start_time / 10**6) + self.d = round(span.duration / 10**6) + self.f = source + self.ec = span.attributes.pop("ec", None) + self.data = DictionaryOfStan() + self.stack = span.stack + + if span.synthetic is True: + self.sy = span.synthetic + + self.__dict__.update(kwargs) + + def _populate_extra_span_attributes(self, span) -> None: + if span.context.trace_parent: + self.tp = span.context.trace_parent + if span.context.instana_ancestor: + self.ia = span.context.instana_ancestor + if span.context.long_trace_id: + self.lt = span.context.long_trace_id + if span.context.correlation_type: + self.crtp = span.context.correlation_type + if span.context.correlation_id: + self.crid = span.context.correlation_id + + def _validate_attributes(self, attributes): + """ + This method will loop through a set of attributes to validate each key and value. + + :param attributes: dict of attributes + :return: dict - a filtered set of attributes + """ + filtered_attributes = DictionaryOfStan() + for key in attributes.keys(): + validated_key, validated_value = self._validate_attribute( + key, attributes[key] + ) + if validated_key is not None and validated_value is not None: + filtered_attributes[validated_key] = validated_value + return filtered_attributes + + def _validate_attribute(self, key, value): + """ + This method will assure that and are valid to set as a attribute. + If fails the check, an attempt will be made to convert it into + something useful. + + On check failure, this method will return None values indicating that the attribute is + not valid and could not be converted into something useful + + :param key: The attribute key + :param value: The attribute value + :return: Tuple (key, value) + """ + validated_key = None + validated_value = None + + try: + # Attribute keys must be some type of text or string type + if isinstance(key, (six.text_type, six.string_types)): + validated_key = key[0:1024] # Max key length of 1024 characters + + if isinstance( + value, + (bool, float, int, list, dict, six.text_type, six.string_types), + ): + validated_value = value + else: + validated_value = self._convert_attribute_value(value) + else: + logger.debug( + "(non-fatal) attribute names must be strings. attribute discarded for %s", + type(key), + ) + except Exception: + logger.debug("instana.span._validate_attribute: ", exc_info=True) + + return (validated_key, validated_value) + + def _convert_attribute_value(self, value): + final_value = None + + try: + final_value = repr(value) + except Exception: + final_value = ( + "(non-fatal) span.set_attribute: values must be one of these types: bool, float, int, list, " + "set, str or alternatively support 'repr'. attribute discarded" + ) + logger.debug(final_value, exc_info=True) + return None + return final_value + diff --git a/src/instana/span/kind.py b/src/instana/span/kind.py new file mode 100644 index 00000000..8b8c6ea7 --- /dev/null +++ b/src/instana/span/kind.py @@ -0,0 +1,56 @@ +# (c) Copyright IBM Corp. 2024 + +ENTRY_KIND = ("entry", "server", "consumer") + +EXIT_KIND = ("exit", "client", "producer") + +LOCAL_SPANS = ("render",) + +HTTP_SPANS = ( + "aiohttp-client", + "aiohttp-server", + "django", + "http", + "tornado-client", + "tornado-server", + "urllib3", + "wsgi", + "asgi", +) + +ENTRY_SPANS = ( + "aiohttp-server", + "aws.lambda.entry", + "celery-worker", + "django", + "wsgi", + "rabbitmq", + "rpc-server", + "tornado-server", + "gcps-consumer", + "asgi", +) + +EXIT_SPANS = ( + "aiohttp-client", + "boto3", + "cassandra", + "celery-client", + "couchbase", + "log", + "memcache", + "mongo", + "mysql", + "postgres", + "rabbitmq", + "redis", + "rpc-client", + "sqlalchemy", + "tornado-client", + "urllib3", + "pymongo", + "gcs", + "gcps-producer", +) + +REGISTERED_SPANS = LOCAL_SPANS + ENTRY_SPANS + EXIT_SPANS diff --git a/src/instana/span/readable_span.py b/src/instana/span/readable_span.py new file mode 100644 index 00000000..529030ef --- /dev/null +++ b/src/instana/span/readable_span.py @@ -0,0 +1,107 @@ +# (c) Copyright IBM Corp. 2024 + +from time import time_ns +from typing import Optional, Sequence + +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util import types + +from instana.span_context import SpanContext + + +class Event: + def __init__( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + self._name = name + self._attributes = attributes + if timestamp is None: + self._timestamp = time_ns() + else: + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def timestamp(self) -> int: + return self._timestamp + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class ReadableSpan: + """ + Provides read-only access to span attributes. + + Users should NOT be creating these objects directly. + `ReadableSpan`s are created as a direct result from using the tracing pipeline + via the `Tracer`. + """ + + def __init__( + self, + name: str, + context: SpanContext, + parent_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + attributes: types.Attributes = {}, + events: Sequence[Event] = [], + status: Optional[Status] = Status(StatusCode.UNSET), + ) -> None: + self._name = name + self._context = context + self._start_time = start_time or time_ns() + self._end_time = end_time + self._duration = 0 + self._attributes = attributes if attributes else {} + self._events = events + self._parent_id = parent_id + self._status = status + self.stack = None + self.synthetic = False + if context.synthetic: + self.synthetic = True + + @property + def name(self) -> str: + return self._name + + @property + def context(self) -> SpanContext: + return self._context + + @property + def start_time(self) -> Optional[int]: + return self._start_time + + @property + def end_time(self) -> Optional[int]: + return self._end_time + + @property + def duration(self) -> int: + return self._duration + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + @property + def events(self) -> Sequence[Event]: + return self._events + + @property + def status(self) -> Status: + return self._status + + @property + def parent_id(self) -> int: + return self._parent_id diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py new file mode 100644 index 00000000..0a6e1f56 --- /dev/null +++ b/src/instana/span/registered_span.py @@ -0,0 +1,334 @@ +# (c) Copyright IBM Corp. 2024 + +from instana.log import logger +from instana.span.base_span import BaseSpan +from instana.span.kind import ENTRY_SPANS, EXIT_SPANS, HTTP_SPANS, LOCAL_SPANS + +from opentelemetry.trace import SpanKind + + +class RegisteredSpan(BaseSpan): + def __init__(self, span, source, service_name, **kwargs) -> None: + # pylint: disable=invalid-name + super(RegisteredSpan, self).__init__(span, source, **kwargs) + self.n = span.name + self.k = SpanKind.SERVER # entry + + self.data["service"] = service_name + if span.name in ENTRY_SPANS: + # entry + self._populate_entry_span_data(span) + self._populate_extra_span_attributes(span) + elif span.name in EXIT_SPANS: + self.k = SpanKind.CLIENT # exit + self._populate_exit_span_data(span) + elif span.name in LOCAL_SPANS: + self.k = 3 # intermediate span + self._populate_local_span_data(span) + + if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": + self.k = SpanKind.CLIENT # exit + + # unify the span name for gcps-producer and gcps-consumer + if "gcps" in span.name: + self.n = "gcps" + + # Store any leftover attributes in the custom section + if len(span.attributes) > 0: + self.data["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) + + def _populate_entry_span_data(self, span) -> None: + if span.name in HTTP_SPANS: + self._collect_http_attributes(span) + + elif span.name == "aws.lambda.entry": + self.data["lambda"]["arn"] = span.attributes.pop("lambda.arn", "Unknown") + self.data["lambda"]["alias"] = None + self.data["lambda"]["runtime"] = "python" + self.data["lambda"]["functionName"] = span.attributes.pop( + "lambda.name", "Unknown" + ) + self.data["lambda"]["functionVersion"] = span.attributes.pop( + "lambda.version", "Unknown" + ) + self.data["lambda"]["trigger"] = span.attributes.pop("lambda.trigger", None) + self.data["lambda"]["error"] = span.attributes.pop("lambda.error", None) + + trigger_type = self.data["lambda"]["trigger"] + + if trigger_type in ["aws:api.gateway", "aws:application.load.balancer"]: + self._collect_http_attributes(span) + elif trigger_type == "aws:cloudwatch.events": + self.data["lambda"]["cw"]["events"]["id"] = span.attributes.pop( + "data.lambda.cw.events.id", None + ) + self.data["lambda"]["cw"]["events"]["more"] = span.attributes.pop( + "lambda.cw.events.more", False + ) + self.data["lambda"]["cw"]["events"]["resources"] = span.attributes.pop( + "lambda.cw.events.resources", None + ) + + elif trigger_type == "aws:cloudwatch.logs": + self.data["lambda"]["cw"]["logs"]["group"] = span.attributes.pop( + "lambda.cw.logs.group", None + ) + self.data["lambda"]["cw"]["logs"]["stream"] = span.attributes.pop( + "lambda.cw.logs.stream", None + ) + self.data["lambda"]["cw"]["logs"]["more"] = span.attributes.pop( + "lambda.cw.logs.more", None + ) + self.data["lambda"]["cw"]["logs"]["events"] = span.attributes.pop( + "lambda.cw.logs.events", None + ) + + elif trigger_type == "aws:s3": + self.data["lambda"]["s3"]["events"] = span.attributes.pop( + "lambda.s3.events", None + ) + elif trigger_type == "aws:sqs": + self.data["lambda"]["sqs"]["messages"] = span.attributes.pop( + "lambda.sqs.messages", None + ) + + elif span.name == "celery-worker": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["retry-reason"] = span.attributes.pop( + "retry-reason", None + ) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcps-consumer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["sub"] = span.attributes.pop("gcps.sub", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "rpc-server": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + else: + logger.debug("SpanRecorder: Unknown entry span: %s" % span.name) + + def _populate_local_span_data(self, span) -> None: + if span.name == "render": + self.data["render"]["name"] = span.attributes.pop("name", None) + self.data["render"]["type"] = span.attributes.pop("type", None) + self.data["event"]["message"] = span.attributes.pop("message", None) + self.data["event"]["parameters"] = span.attributes.pop("parameters", None) + else: + logger.debug("SpanRecorder: Unknown local span: %s" % span.name) + + def _populate_exit_span_data(self, span) -> None: + if span.name in HTTP_SPANS: + self._collect_http_attributes(span) + + elif span.name == "boto3": + # boto3 also sends http attributes + self._collect_http_attributes(span) + + for attribute in ["op", "ep", "reg", "payload", "error"]: + value = span.attributes.pop(attribute, None) + if value is not None: + if attribute == "payload": + self.data["boto3"][attribute] = self._validate_attributes(value) + else: + self.data["boto3"][attribute] = value + + elif span.name == "cassandra": + self.data["cassandra"]["cluster"] = span.attributes.pop( + "cassandra.cluster", None + ) + self.data["cassandra"]["query"] = span.attributes.pop( + "cassandra.query", None + ) + self.data["cassandra"]["keyspace"] = span.attributes.pop( + "cassandra.keyspace", None + ) + self.data["cassandra"]["fetchSize"] = span.attributes.pop( + "cassandra.fetchSize", None + ) + self.data["cassandra"]["achievedConsistency"] = span.attributes.pop( + "cassandra.achievedConsistency", None + ) + self.data["cassandra"]["triedHosts"] = span.attributes.pop( + "cassandra.triedHosts", None + ) + self.data["cassandra"]["fullyFetched"] = span.attributes.pop( + "cassandra.fullyFetched", None + ) + self.data["cassandra"]["error"] = span.attributes.pop( + "cassandra.error", None + ) + + elif span.name == "celery-client": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "couchbase": + self.data["couchbase"]["hostname"] = span.attributes.pop( + "couchbase.hostname", None + ) + self.data["couchbase"]["bucket"] = span.attributes.pop( + "couchbase.bucket", None + ) + self.data["couchbase"]["type"] = span.attributes.pop("couchbase.type", None) + self.data["couchbase"]["error"] = span.attributes.pop( + "couchbase.error", None + ) + self.data["couchbase"]["error_type"] = span.attributes.pop( + "couchbase.error_type", None + ) + self.data["couchbase"]["sql"] = span.attributes.pop("couchbase.sql", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "redis": + self.data["redis"]["connection"] = span.attributes.pop("connection", None) + self.data["redis"]["driver"] = span.attributes.pop("driver", None) + self.data["redis"]["command"] = span.attributes.pop("command", None) + self.data["redis"]["error"] = span.attributes.pop("redis.error", None) + self.data["redis"]["subCommands"] = span.attributes.pop("subCommands", None) + + elif span.name == "rpc-client": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + + elif span.name == "sqlalchemy": + self.data["sqlalchemy"]["sql"] = span.attributes.pop("sqlalchemy.sql", None) + self.data["sqlalchemy"]["eng"] = span.attributes.pop("sqlalchemy.eng", None) + self.data["sqlalchemy"]["url"] = span.attributes.pop("sqlalchemy.url", None) + self.data["sqlalchemy"]["err"] = span.attributes.pop("sqlalchemy.err", None) + + elif span.name == "mysql": + self.data["mysql"]["host"] = span.attributes.pop("host", None) + self.data["mysql"]["port"] = span.attributes.pop("port", None) + self.data["mysql"]["db"] = span.attributes.pop("db.instance", None) + self.data["mysql"]["user"] = span.attributes.pop("db.user", None) + self.data["mysql"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) + + elif span.name == "postgres": + self.data["pg"]["host"] = span.attributes.pop("host", None) + self.data["pg"]["port"] = span.attributes.pop("port", None) + self.data["pg"]["db"] = span.attributes.pop("db.instance", None) + self.data["pg"]["user"] = span.attributes.pop("db.user", None) + self.data["pg"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["pg"]["error"] = span.attributes.pop("pg.error", None) + + elif span.name == "mongo": + service = "%s:%s" % ( + span.attributes.pop("host", None), + span.attributes.pop("port", None), + ) + namespace = "%s.%s" % ( + span.attributes.pop("db", "?"), + span.attributes.pop("collection", "?"), + ) + + self.data["mongo"]["service"] = service + self.data["mongo"]["namespace"] = namespace + self.data["mongo"]["command"] = span.attributes.pop("command", None) + self.data["mongo"]["filter"] = span.attributes.pop("filter", None) + self.data["mongo"]["json"] = span.attributes.pop("json", None) + self.data["mongo"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcs": + self.data["gcs"]["op"] = span.attributes.pop("gcs.op", None) + self.data["gcs"]["bucket"] = span.attributes.pop("gcs.bucket", None) + self.data["gcs"]["object"] = span.attributes.pop("gcs.object", None) + self.data["gcs"]["entity"] = span.attributes.pop("gcs.entity", None) + self.data["gcs"]["range"] = span.attributes.pop("gcs.range", None) + self.data["gcs"]["sourceBucket"] = span.attributes.pop( + "gcs.sourceBucket", None + ) + self.data["gcs"]["sourceObject"] = span.attributes.pop( + "gcs.sourceObject", None + ) + self.data["gcs"]["sourceObjects"] = span.attributes.pop( + "gcs.sourceObjects", None + ) + self.data["gcs"]["destinationBucket"] = span.attributes.pop( + "gcs.destinationBucket", None + ) + self.data["gcs"]["destinationObject"] = span.attributes.pop( + "gcs.destinationObject", None + ) + self.data["gcs"]["numberOfOperations"] = span.attributes.pop( + "gcs.numberOfOperations", None + ) + self.data["gcs"]["projectId"] = span.attributes.pop("gcs.projectId", None) + self.data["gcs"]["accessId"] = span.attributes.pop("gcs.accessId", None) + + elif span.name == "gcps-producer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["top"] = span.attributes.pop("gcps.top", None) + + elif span.name == "log": + # use last special key values + for event in span.events: + if "message" in event.attributes: + self.data["event"]["message"] = event.attributes.pop( + "message", None + ) + if "parameters" in event.attributes: + self.data["event"]["parameters"] = event.attributes.pop( + "parameters", None + ) + else: + logger.debug("SpanRecorder: Unknown exit span: %s" % span.name) + + def _collect_http_attributes(self, span) -> None: + self.data["http"]["host"] = span.attributes.pop("http.host", None) + self.data["http"]["url"] = span.attributes.pop("http.url", None) + self.data["http"]["path"] = span.attributes.pop("http.path", None) + self.data["http"]["params"] = span.attributes.pop("http.params", None) + self.data["http"]["method"] = span.attributes.pop("http.method", None) + self.data["http"]["status"] = span.attributes.pop("http.status_code", None) + self.data["http"]["path_tpl"] = span.attributes.pop("http.path_tpl", None) + self.data["http"]["error"] = span.attributes.pop("http.error", None) + + if len(span.attributes) > 0: + custom_headers = [] + for key in span.attributes: + if key[0:12] == "http.header.": + custom_headers.append(key) + + for key in custom_headers: + trimmed_key = key[12:] + self.data["http"]["header"][trimmed_key] = span.attributes.pop(key) diff --git a/src/instana/span/sdk_span.py b/src/instana/span/sdk_span.py new file mode 100644 index 00000000..89485144 --- /dev/null +++ b/src/instana/span/sdk_span.py @@ -0,0 +1,62 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import Tuple + +from instana.span.base_span import BaseSpan +from instana.span.kind import ENTRY_KIND, EXIT_KIND +from instana.util import DictionaryOfStan + + +class SDKSpan(BaseSpan): + def __init__(self, span, source, service_name, **kwargs) -> None: + # pylint: disable=invalid-name + super(SDKSpan, self).__init__(span, source, **kwargs) + + span_kind = self.get_span_kind(span) + + self.n = "sdk" + self.k = span_kind[1] + + if service_name is not None: + self.data["service"] = service_name + + self.data["sdk"]["name"] = span.name + self.data["sdk"]["type"] = span_kind[0] + self.data["sdk"]["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) + + if span.events is not None and len(span.events) > 0: + events = DictionaryOfStan() + for event in span.events: + filtered_attributes = self._validate_attributes(event.attributes) + if len(filtered_attributes.keys()) > 0: + events[repr(event.timestamp)] = filtered_attributes + self.data["sdk"]["custom"]["events"] = events + + if "arguments" in span.attributes: + self.data["sdk"]["arguments"] = span.attributes["arguments"] + + if "return" in span.attributes: + self.data["sdk"]["return"] = span.attributes["return"] + + # if len(span.context.baggage) > 0: + # self.data["baggage"] = span.context.baggage + + def get_span_kind(self, span) -> Tuple[str, int]: + """ + Will retrieve the `span.kind` attribute and return a tuple containing the appropriate string and integer + values for the Instana backend + + :param span: The span to search for the `span.kind` attribute + :return: Tuple (String, Int) + """ + kind = ("intermediate", 3) + if "span.kind" in span.attributes: + if span.attributes["span.kind"] in ENTRY_KIND: + kind = ("entry", 1) + elif span.attributes["span.kind"] in EXIT_KIND: + kind = ("exit", 2) + return kind + + diff --git a/src/instana/span/span.py b/src/instana/span/span.py new file mode 100644 index 00000000..5ec1a5db --- /dev/null +++ b/src/instana/span/span.py @@ -0,0 +1,251 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2017 + +""" +This module contains the classes that represents spans. + +InstanaSpan - the OpenTelemetry based span used during tracing + +When an InstanaSpan is finished, it is converted into either an SDKSpan +or RegisteredSpan depending on type. + +BaseSpan: Base class containing the commonalities for the two descendants + - SDKSpan: Class that represents an SDK type span + - RegisteredSpan: Class that represents a Registered type span +""" + +from threading import Lock +from time import time_ns +from typing import Dict, Optional, Sequence, Union + +from opentelemetry.context import get_value +from opentelemetry.context.context import Context +from opentelemetry.trace import ( + _SPAN_KEY, + DEFAULT_TRACE_OPTIONS, + DEFAULT_TRACE_STATE, + INVALID_SPAN_ID, + INVALID_TRACE_ID, + Span, +) +from opentelemetry.trace.span import NonRecordingSpan +from opentelemetry.trace.status import Status, StatusCode +from opentelemetry.util import types + +from instana.log import logger +from instana.recorder import StanRecorder +from instana.span.kind import HTTP_SPANS +from instana.span.readable_span import Event, ReadableSpan +from instana.span_context import SpanContext + + +class InstanaSpan(Span, ReadableSpan): + def __init__( + self, + name: str, + context: SpanContext, + span_processor: StanRecorder, + parent_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + attributes: types.Attributes = {}, + events: Sequence[Event] = [], + status: Optional[Status] = Status(StatusCode.UNSET), + ) -> None: + super().__init__( + name=name, + context=context, + parent_id=parent_id, + start_time=start_time, + end_time=end_time, + attributes=attributes, + events=events, + status=status, + # kind=kind, + ) + self._span_processor = span_processor + self._lock = Lock() + + def get_span_context(self) -> SpanContext: + return self._context + + def set_attributes(self, attributes: Dict[str, types.AttributeValue]) -> None: + if not self._attributes: + self._attributes = {} + + with self._lock: + for key, value in attributes.items(): + self._attributes[key] = value + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + return self.set_attributes({key: value}) + + def update_name(self, name: str) -> None: + with self._lock: + self._name = name + + def is_recording(self) -> bool: + return self._end_time is None + + def set_status( + self, + status: Union[Status, StatusCode], + description: Optional[str] = None, + ) -> None: + # Ignore future calls if status is already set to OK + # Ignore calls to set to StatusCode.UNSET + if isinstance(status, Status): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return + if description is not None: + logger.warning( + "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", + description, + ) + self._status = status + elif isinstance(status, StatusCode): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status is StatusCode.UNSET + ): + return + self._status = Status(status, description) + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + event = Event( + name=name, + attributes=attributes, + timestamp=timestamp, + ) + + self._events.append(event) + + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + escaped: bool = False, + ) -> None: + """ + Records an exception as a span event. This will record pertinent info from the exception and + assure that this span is marked as errored. + """ + try: + message = "" + self.mark_as_errored() + if hasattr(exception, "__str__") and len(str(exception)) > 0: + message = str(exception) + elif hasattr(exception, "message") and exception.message is not None: + message = exception.message + else: + message = repr(exception) + + if self.name in ["rpc-server", "rpc-client"]: + self.set_attribute("rpc.error", message) + elif self.name == "mysql": + self.set_attribute("mysql.error", message) + elif self.name == "postgres": + self.set_attribute("pg.error", message) + elif self.name in HTTP_SPANS: + self.set_attribute("http.error", message) + elif self.name in ["celery-client", "celery-worker"]: + self.set_attribute("error", message) + elif self.name == "sqlalchemy": + self.set_attribute("sqlalchemy.err", message) + elif self.name == "aws.lambda.entry": + self.set_attribute("lambda.error", message) + else: + _attributes = {"message": message} + if attributes: + _attributes.update(attributes) + self.add_event( + name="exception", attributes=_attributes, timestamp=timestamp + ) + except Exception: + logger.debug("span.record_exception", exc_info=True) + raise + + def _readable_span(self) -> ReadableSpan: + return ReadableSpan( + name=self.name, + context=self.context, + parent_id=self.parent_id, + start_time=self.start_time, + end_time=self.end_time, + attributes=self.attributes, + events=self.events, + status=self.status, + # kind=self.kind, + ) + + def end(self, end_time: Optional[int] = None) -> None: + with self._lock: + self._end_time = end_time if end_time is not None else time_ns() + self._duration = self._end_time - self._start_time + + self._span_processor.record_span(self._readable_span()) + + def mark_as_errored(self, attributes: types.Attributes = None) -> None: + """ + Mark this span as errored. + + @param attributes: optional attributes to add to the span + """ + try: + ec = self.attributes.get("ec", 0) + self.set_attribute("ec", ec + 1) + + if attributes is not None and isinstance(attributes, dict): + for key in attributes: + self.set_attribute(key, attributes[key]) + except Exception: + logger.debug("span.mark_as_errored", exc_info=True) + + def assure_errored(self) -> None: + """ + Make sure that this span is marked as errored. + @return: None + """ + try: + ec = self.attributes.get("ec", None) + if ec is None or ec == 0: + self.set_attribute("ec", 1) + except Exception: + logger.debug("span.assure_errored", exc_info=True) + + +INVALID_SPAN_CONTEXT = SpanContext( + trace_id=INVALID_TRACE_ID, + span_id=INVALID_SPAN_ID, + is_remote=False, + trace_flags=DEFAULT_TRACE_OPTIONS, + trace_state=DEFAULT_TRACE_STATE, +) +INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT) + + +def get_current_span(context: Optional[Context] = None) -> InstanaSpan: + """Retrieve the current span. + + Args: + context: A Context object. If one is not passed, the + default current context is used instead. + + Returns: + The Span set in the context if it exists. INVALID_SPAN otherwise. + """ + span = get_value(_SPAN_KEY, context=context) + if span is None or not isinstance(span, InstanaSpan): + return INVALID_SPAN + return span diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 9d82cc9f..60f9eb30 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -31,7 +31,8 @@ from instana.propagators.text_propagator import TextPropagator from instana.recorder import StanRecorder from instana.sampling import InstanaSampler, Sampler -from instana.span import InstanaSpan, RegisteredSpan, get_current_span +from instana.span.kind import EXIT_SPANS +from instana.span.span import InstanaSpan, get_current_span from instana.span_context import SpanContext from instana.util.ids import generate_id @@ -63,8 +64,8 @@ def get_tracer( return InstanaTracer( self.sampler, - self._exporter, self._span_processor, + self._exporter, self._propagators, ) @@ -144,7 +145,7 @@ def start_span( if parent_context is not None: span.synthetic = parent_context.synthetic - if name in RegisteredSpan.EXIT_SPANS: + if name in EXIT_SPANS: self._add_stack(span) return span diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index 89d3c3be..3d82da2d 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -5,7 +5,7 @@ from instana.log import logger from instana.singletons import agent, tracer, async_tracer, tornado_tracer -from instana.span import InstanaSpan, get_current_span +from instana.span.span import InstanaSpan, get_current_span from instana.tracer import InstanaTracer From 9a4305dc2cfba866ca8be661ab2063c922ec67de Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Sun, 14 Jul 2024 12:04:44 -0700 Subject: [PATCH 031/172] fix(tests): Adapt unit tests after Span structure refactor. Signed-off-by: Paulo Vital --- tests/conftest.py | 24 ++++- tests/test_span.py | 174 ++++++++++++++++++++++------------ tests/test_span_base.py | 23 +++-- tests/test_span_event.py | 3 +- tests/test_span_registered.py | 100 +++++++++++-------- tests/test_span_sdk.py | 13 ++- tests/test_tracer.py | 128 +++++++++++++------------ tests/test_tracer_provider.py | 23 +++-- 8 files changed, 297 insertions(+), 191 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 89d966c9..0a4c88ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,8 +19,12 @@ # TODO: remove all "noqa: E402" from instana package imports and move the # block of env variables setting to below the imports after finishing the # migration of instrumentation codes. -from instana.span import BaseSpan, InstanaSpan # noqa: E402 +from instana.agent.test import TestAgent # noqa: E402 +from instana.recorder import StanRecorder # noqa: E402 +from instana.span.base_span import BaseSpan # noqa: E402 +from instana.span.span import InstanaSpan # noqa: E402 from instana.span_context import SpanContext # noqa: E402 +from instana.tracer import InstanaTracerProvider # noqa: E402 collect_ignore_glob = [ "*autoprofile*", @@ -111,6 +115,18 @@ def span_id() -> int: return 6895521157646639861 +@pytest.fixture +def span_processor() -> StanRecorder: + rec = StanRecorder(TestAgent()) + rec.THREAD_NAME = "InstanaSpan Recorder Test" + return rec + + +@pytest.fixture +def tracer_provider(span_processor: StanRecorder) -> InstanaTracerProvider: + return InstanaTracerProvider(span_processor=span_processor, exporter=TestAgent()) + + @pytest.fixture def span_context(trace_id: int, span_id: int) -> SpanContext: return SpanContext( @@ -121,14 +137,14 @@ def span_context(trace_id: int, span_id: int) -> SpanContext: @pytest.fixture -def span(span_context: SpanContext) -> InstanaSpan: +def span(span_context: SpanContext, span_processor: StanRecorder) -> InstanaSpan: span_name = "test-span" - return InstanaSpan(span_name, span_context) + return InstanaSpan(span_name, span_context, span_processor) @pytest.fixture def base_span(span: InstanaSpan) -> BaseSpan: - return BaseSpan(span, None, "test") + return BaseSpan(span, None) @pytest.fixture diff --git a/tests/test_span.py b/tests/test_span.py index bda0333b..48abadcd 100644 --- a/tests/test_span.py +++ b/tests/test_span.py @@ -6,18 +6,20 @@ import pytest from opentelemetry.trace.status import Status, StatusCode -from instana.span import INVALID_SPAN, Event, InstanaSpan, get_current_span +from instana.recorder import StanRecorder +from instana.span.span import INVALID_SPAN, Event, InstanaSpan, get_current_span from instana.span_context import SpanContext def test_span_default( span_context: SpanContext, + span_processor: StanRecorder, trace_id: int, span_id: int, ) -> None: span_name = "test-span" timestamp = time.time_ns() - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert span is not None assert isinstance(span, InstanaSpan) @@ -41,12 +43,12 @@ def test_span_default( def test_span_get_span_context( span_context: SpanContext, + span_processor: StanRecorder, trace_id: int, span_id: int, ) -> None: - span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) context = span.get_span_context() assert isinstance(context, SpanContext) @@ -55,9 +57,11 @@ def test_span_get_span_context( assert context == span.context -def test_span_set_attributes_default(span_context: SpanContext) -> None: +def test_span_set_attributes_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert not span.attributes @@ -73,13 +77,15 @@ def test_span_set_attributes_default(span_context: SpanContext) -> None: assert "two" == span.attributes.get("field2") -def test_span_set_attributes(span_context: SpanContext) -> None: +def test_span_set_attributes( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" attributes = { "field1": 1, "field2": "two", } - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) assert span.attributes assert len(span.attributes) == 2 @@ -97,9 +103,11 @@ def test_span_set_attributes(span_context: SpanContext) -> None: assert "vier" in span.attributes.get("field4") -def test_span_set_attribute_default(span_context: SpanContext) -> None: +def test_span_set_attribute_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert not span.attributes @@ -116,13 +124,15 @@ def test_span_set_attribute_default(span_context: SpanContext) -> None: assert "two" == span.attributes.get("field2") -def test_span_set_attribute(span_context: SpanContext) -> None: +def test_span_set_attribute( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" attributes = { "field1": 1, "field2": "two", } - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) assert span.attributes assert len(span.attributes) == 2 @@ -141,9 +151,11 @@ def test_span_set_attribute(span_context: SpanContext) -> None: assert "vier" in span.attributes.get("field4") -def test_span_update_name(span_context: SpanContext) -> None: +def test_span_update_name( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span-1" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert span is not None assert isinstance(span, InstanaSpan) @@ -156,9 +168,11 @@ def test_span_update_name(span_context: SpanContext) -> None: assert span.name == new_span_name -def test_span_set_status_with_Status_default(span_context, caplog) -> None: +def test_span_set_status_with_Status_default( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert span.status assert span.status.is_unset @@ -187,9 +201,11 @@ def test_span_set_status_with_Status_default(span_context, caplog) -> None: assert span.status.status_code != StatusCode.ERROR -def test_span_set_status_with_Status_and_desc(span_context, caplog) -> None: +def test_span_set_status_with_Status_and_desc( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert span.status assert span.status.is_unset @@ -221,7 +237,9 @@ def test_span_set_status_with_Status_and_desc(span_context, caplog) -> None: assert span.status.status_code != StatusCode.ERROR -def test_span_set_status_with_StatusUNSET_to_StatusERROR(span_context, caplog) -> None: +def test_span_set_status_with_StatusUNSET_to_StatusERROR( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: span_name = "test-span" status_desc = "Status is UNSET." span_status = Status(status_code=StatusCode.UNSET, description=status_desc) @@ -231,7 +249,7 @@ def test_span_set_status_with_StatusUNSET_to_StatusERROR(span_context, caplog) - == caplog.record_tuples[0][2] ) - span = InstanaSpan(span_name, span_context, status=span_status) + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) assert span.status assert span.status.is_unset @@ -254,7 +272,9 @@ def test_span_set_status_with_StatusUNSET_to_StatusERROR(span_context, caplog) - assert span.status.status_code == StatusCode.ERROR -def test_span_set_status_with_StatusOK_to_StatusERROR(span_context, caplog) -> None: +def test_span_set_status_with_StatusOK_to_StatusERROR( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: span_name = "test-span" status_desc = "Status is OK." span_status = Status(status_code=StatusCode.OK, description=status_desc) @@ -264,7 +284,7 @@ def test_span_set_status_with_StatusOK_to_StatusERROR(span_context, caplog) -> N == caplog.record_tuples[0][2] ) - span = InstanaSpan(span_name, span_context, status=span_status) + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) assert span.status assert not span.status.is_unset @@ -287,9 +307,11 @@ def test_span_set_status_with_StatusOK_to_StatusERROR(span_context, caplog) -> N assert span.status.status_code != StatusCode.ERROR -def test_span_set_status_with_StatusCode_default(span_context: SpanContext) -> None: +def test_span_set_status_with_StatusCode_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert span.status assert span.status.is_unset @@ -312,9 +334,11 @@ def test_span_set_status_with_StatusCode_default(span_context: SpanContext) -> N assert span.status.status_code != StatusCode.ERROR -def test_span_set_status_with_StatusCode_and_desc(span_context, caplog) -> None: +def test_span_set_status_with_StatusCode_and_desc( + span_context: SpanContext, span_processor: StanRecorder, caplog +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert span.status assert span.status.is_unset @@ -342,7 +366,7 @@ def test_span_set_status_with_StatusCode_and_desc(span_context, caplog) -> None: def test_span_set_status_with_StatusCodeUNSET_to_StatusCodeERROR( - span_context, caplog + span_context: SpanContext, span_processor: StanRecorder, caplog ) -> None: span_name = "test-span" status_desc = "Status is UNSET." @@ -353,7 +377,7 @@ def test_span_set_status_with_StatusCodeUNSET_to_StatusCodeERROR( == caplog.record_tuples[0][2] ) - span = InstanaSpan(span_name, span_context, status=span_status) + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) assert span.status assert span.status.is_unset @@ -377,7 +401,7 @@ def test_span_set_status_with_StatusCodeUNSET_to_StatusCodeERROR( def test_span_set_status_with_StatusCodeOK_to_StatusCodeERROR( - span_context, caplog + span_context: SpanContext, span_processor: StanRecorder, caplog ) -> None: span_name = "test-span" status_desc = "Status is OK." @@ -388,7 +412,7 @@ def test_span_set_status_with_StatusCodeOK_to_StatusCodeERROR( == caplog.record_tuples[0][2] ) - span = InstanaSpan(span_name, span_context, status=span_status) + span = InstanaSpan(span_name, span_context, span_processor, status=span_status) assert span.status assert not span.status.is_unset @@ -411,9 +435,11 @@ def test_span_set_status_with_StatusCodeOK_to_StatusCodeERROR( assert span.status.status_code != StatusCode.ERROR -def test_span_add_event_default(span_context: SpanContext) -> None: +def test_span_add_event_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert not span.events @@ -434,7 +460,9 @@ def test_span_add_event_default(span_context: SpanContext) -> None: assert len(event.attributes) == 2 -def test_span_add_event(span_context: SpanContext) -> None: +def test_span_add_event( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" event_name1 = "event1" attributes = { @@ -443,7 +471,7 @@ def test_span_add_event(span_context: SpanContext) -> None: } timestamp1 = time.time_ns() event = Event(event_name1, attributes, timestamp1) - span = InstanaSpan(span_name, span_context, events=[event]) + span = InstanaSpan(span_name, span_context, span_processor, events=[event]) assert span.events assert len(span.events) == 1 @@ -490,13 +518,14 @@ def test_span_add_event(span_context: SpanContext) -> None: ) def test_span_record_exception_default( span_context: SpanContext, + span_processor: StanRecorder, span_name: str, span_attribute: str, ) -> None: exception_msg = "Test Exception" exception = Exception(exception_msg) - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) span.record_exception(exception) @@ -512,7 +541,9 @@ def test_span_record_exception_default( assert exception_msg == event.attributes.get("message", None) -def test_span_record_exception_with_attribute(span_context: SpanContext) -> None: +def test_span_record_exception_with_attribute( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" exception_msg = "Test Exception" attributes = { @@ -520,7 +551,7 @@ def test_span_record_exception_with_attribute(span_context: SpanContext) -> None } exception = Exception(exception_msg) - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) span.record_exception(exception, attributes) @@ -534,14 +565,16 @@ def test_span_record_exception_with_attribute(span_context: SpanContext) -> None assert 0 == event.attributes.get("custom_attr", None) -def test_span_record_exception_with_Exception_msg(span_context: SpanContext) -> None: +def test_span_record_exception_with_Exception_msg( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "wsgi" span_attribute = "http.error" exception_msg = "Test Exception" exception = Exception() exception.message = exception_msg - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) span.record_exception(exception) @@ -553,13 +586,14 @@ def test_span_record_exception_with_Exception_msg(span_context: SpanContext) -> def test_span_record_exception_with_Exception_none_msg( span_context: SpanContext, + span_processor: StanRecorder, ) -> None: span_name = "wsgi" span_attribute = "http.error" exception = Exception() exception.message = None - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) span.record_exception(exception) @@ -569,22 +603,26 @@ def test_span_record_exception_with_Exception_none_msg( assert "Exception()" == span.attributes.get(span_attribute, None) -def test_span_record_exception_with_Exception_raised(span_context: SpanContext) -> None: +def test_span_record_exception_with_Exception_raised( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" exception = None - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) with patch( - "instana.span.InstanaSpan.add_event", side_effect=Exception("mocked error") + "instana.span.span.InstanaSpan.add_event", side_effect=Exception("mocked error") ): with pytest.raises(Exception): span.record_exception(exception) -def test_span_end_default(span_context: SpanContext) -> None: +def test_span_end_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert not span.end_time @@ -597,9 +635,9 @@ def test_span_end_default(span_context: SpanContext) -> None: assert span.duration > 0 -def test_span_end(span_context: SpanContext) -> None: +def test_span_end(span_context: SpanContext, span_processor: StanRecorder) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) assert not span.end_time @@ -614,12 +652,14 @@ def test_span_end(span_context: SpanContext) -> None: assert span.duration == (timestamp_end - span.start_time) -def test_span_mark_as_errored_default(span_context: SpanContext) -> None: +def test_span_mark_as_errored_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" attributes = { "ec": 0, } - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) assert span.attributes assert len(span.attributes) == 1 @@ -632,12 +672,14 @@ def test_span_mark_as_errored_default(span_context: SpanContext) -> None: assert span.attributes.get("ec") == 1 -def test_span_mark_as_errored(span_context: SpanContext) -> None: +def test_span_mark_as_errored( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" attributes = { "ec": 0, } - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) assert span.attributes assert len(span.attributes) == 1 @@ -664,20 +706,25 @@ def test_span_mark_as_errored(span_context: SpanContext) -> None: assert span.attributes.get("field2") == "two" -def test_span_mark_as_errored_exception(span_context: SpanContext) -> None: +def test_span_mark_as_errored_exception( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) with patch( - "instana.span.InstanaSpan.set_attribute", side_effect=Exception("mocked error") + "instana.span.span.InstanaSpan.set_attribute", + side_effect=Exception("mocked error"), ): span.mark_as_errored() assert not span.attributes -def test_span_assure_errored_default(span_context: SpanContext) -> None: +def test_span_assure_errored_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) span.assure_errored() @@ -686,12 +733,14 @@ def test_span_assure_errored_default(span_context: SpanContext) -> None: assert span.attributes.get("ec") == 1 -def test_span_assure_errored(span_context: SpanContext) -> None: +def test_span_assure_errored( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" attributes = { "ec": 0, } - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) assert span.attributes assert len(span.attributes) == 1 @@ -704,12 +753,15 @@ def test_span_assure_errored(span_context: SpanContext) -> None: assert span.attributes.get("ec") == 1 -def test_span_assure_errored_exception(span_context: SpanContext) -> None: +def test_span_assure_errored_exception( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-span" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) with patch( - "instana.span.InstanaSpan.set_attribute", side_effect=Exception("mocked error") + "instana.span.span.InstanaSpan.set_attribute", + side_effect=Exception("mocked error"), ): span.assure_errored() assert not span.attributes diff --git a/tests/test_span_base.py b/tests/test_span_base.py index f4f8a66c..75c88491 100644 --- a/tests/test_span_base.py +++ b/tests/test_span_base.py @@ -2,7 +2,9 @@ from unittest.mock import Mock, patch -from instana.span import BaseSpan, InstanaSpan +from instana.recorder import StanRecorder +from instana.span.base_span import BaseSpan +from instana.span.span import InstanaSpan from instana.span_context import SpanContext from instana.util import DictionaryOfStan @@ -12,7 +14,7 @@ def test_basespan( trace_id: int, span_id: int, ) -> None: - base_span = BaseSpan(span, None, "test") + base_span = BaseSpan(span, None) expected_dict = { "t": trace_id, @@ -52,7 +54,7 @@ def test_basespan_with_synthetic_source_and_kwargs( span.synthetic = True source = "source test" _kwarg1 = "value1" - base_span = BaseSpan(span, source, "test", arg1=_kwarg1) + base_span = BaseSpan(span, source, arg1=_kwarg1) assert trace_id == base_span.t assert span_id == base_span.s @@ -62,7 +64,7 @@ def test_basespan_with_synthetic_source_and_kwargs( def test_populate_extra_span_attributes(span: InstanaSpan) -> None: - base_span = BaseSpan(span, None, "test") + base_span = BaseSpan(span, None) base_span._populate_extra_span_attributes(span) assert not hasattr(base_span, "tp") @@ -76,6 +78,7 @@ def test_populate_extra_span_attributes(span: InstanaSpan) -> None: def test_populate_extra_span_attributes_with_values( trace_id: int, span_id: int, + span_processor: StanRecorder, ) -> None: long_id = 1512366075204170929049582354406559215 span_context = SpanContext( @@ -89,8 +92,8 @@ def test_populate_extra_span_attributes_with_values( correlation_type="IDK", correlation_id=long_id, ) - span = InstanaSpan("test-base-span", span_context) - base_span = BaseSpan(span, None, "test") + span = InstanaSpan("test-base-span", span_context, span_processor) + base_span = BaseSpan(span, None) base_span._populate_extra_span_attributes(span) assert trace_id == base_span.t @@ -128,12 +131,12 @@ def test_validate_attribute_with_invalid_key_type(base_span: BaseSpan) -> None: def test_validate_attribute_exception(span: InstanaSpan) -> None: - base_span = BaseSpan(span, None, "test") + base_span = BaseSpan(span, None) key = "field1" value = span with patch( - "instana.span.BaseSpan._convert_attribute_value", + "instana.span.base_span.BaseSpan._convert_attribute_value", side_effect=Exception("mocked error"), ): (validated_key, validated_value) = base_span._validate_attribute(key, value) @@ -142,11 +145,11 @@ def test_validate_attribute_exception(span: InstanaSpan) -> None: def test_convert_attribute_value(span: InstanaSpan) -> None: - base_span = BaseSpan(span, None, "test") + base_span = BaseSpan(span, None) value = span converted_value = base_span._convert_attribute_value(value) - assert " None: diff --git a/tests/test_span_event.py b/tests/test_span_event.py index cdfc724a..baa7521b 100644 --- a/tests/test_span_event.py +++ b/tests/test_span_event.py @@ -1,7 +1,8 @@ # (c) Copyright IBM Corp. 2024 import time -from instana.span import Event + +from instana.span.readable_span import Event def test_span_event_defaults(): diff --git a/tests/test_span_registered.py b/tests/test_span_registered.py index c184d940..9c111ca7 100644 --- a/tests/test_span_registered.py +++ b/tests/test_span_registered.py @@ -4,30 +4,34 @@ from typing import Any, Dict, Tuple import pytest +from opentelemetry.trace import SpanKind -from instana.span import InstanaSpan, RegisteredSpan +from instana.recorder import StanRecorder +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan from instana.span_context import SpanContext @pytest.mark.parametrize( "span_name, expected_result, attributes", [ - ("wsgi", ("wsgi", 1, "http"), {}), - ("rabbitmq", ("rabbitmq", 1, "rabbitmq"), {}), - ("gcps-producer", ("gcps", 2, "gcps"), {}), - ("urllib3", ("urllib3", 2, "http"), {}), - ("rabbitmq", ("rabbitmq", 2, "rabbitmq"), {"sort": "publish"}), + ("wsgi", ("wsgi", SpanKind.SERVER, "http"), {}), + ("rabbitmq", ("rabbitmq", SpanKind.SERVER, "rabbitmq"), {}), + ("gcps-producer", ("gcps", SpanKind.CLIENT, "gcps"), {}), + ("urllib3", ("urllib3", SpanKind.CLIENT, "http"), {}), + ("rabbitmq", ("rabbitmq", SpanKind.CLIENT, "rabbitmq"), {"sort": "publish"}), ("render", ("render", 3, "render"), {"arguments": "--quiet"}), ], ) def test_registered_span( span_context: SpanContext, + span_processor: StanRecorder, span_name: str, expected_result: Tuple[str, int, str], - attributes: Dict[str, Any] + attributes: Dict[str, Any], ) -> None: service_name = "test-registered-service" - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) reg_span = RegisteredSpan(span, None, service_name) assert expected_result[0] == reg_span.n @@ -36,7 +40,9 @@ def test_registered_span( assert expected_result[2] in reg_span.data.keys() -def test_collect_http_attributes_with_attributes(span_context: SpanContext) -> None: +def test_collect_http_attributes_with_attributes( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = "test-registered-span" attributes = { "span.kind": "entry", @@ -45,7 +51,7 @@ def test_collect_http_attributes_with_attributes(span_context: SpanContext) -> N "http.header.test": "one more test", } service_name = "test-registered-service" - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) reg_span = RegisteredSpan(span, None, service_name) excepted_result = { @@ -53,20 +59,24 @@ def test_collect_http_attributes_with_attributes(span_context: SpanContext) -> N "http.url": attributes["http.url"], "http.header.test": attributes["http.header.test"], } - + reg_span._collect_http_attributes(span) assert excepted_result["http.host"] == reg_span.data["http"]["host"] assert excepted_result["http.url"] == reg_span.data["http"]["url"] - assert excepted_result["http.header.test"] == reg_span.data["http"]["header"]["test"] + assert ( + excepted_result["http.header.test"] == reg_span.data["http"]["header"]["test"] + ) -def test_populate_local_span_data_with_other_name(span_context: SpanContext, caplog) -> None: +def test_populate_local_span_data_with_other_name( + span_context: SpanContext, caplog +) -> None: # span_name = "test-registered-span" # service_name = "test-registered-service" # span = InstanaSpan(span_name, span_context) # reg_span = RegisteredSpan(span, None, service_name) - + # expected_msg = f"SpanRecorder: Unknown local span: {span_name}" # reg_span._populate_local_span_data(span) @@ -80,7 +90,7 @@ def test_populate_local_span_data_with_other_name(span_context: SpanContext, cap [ ( "aws.lambda.entry", - "lambda", + "lambda", { "lambda.arn": "test", "lambda.trigger": None, @@ -100,7 +110,7 @@ def test_populate_local_span_data_with_other_name(span_context: SpanContext, cap { "gcps.op": "consume", "gcps.projid": "MY_PROJECT", - "gcps.sub": "MY_SUBSCRIPTION_NAME", + "gcps.sub": "MY_SUBSCRIPTION_NAME", }, ), ( @@ -116,11 +126,12 @@ def test_populate_local_span_data_with_other_name(span_context: SpanContext, cap ) def test_populate_entry_span_data( span_context: SpanContext, + span_processor: StanRecorder, span_name: str, service_name: str, - attributes: Dict[str, Any] + attributes: Dict[str, Any], ) -> None: - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) reg_span = RegisteredSpan(span, None, service_name) expected_result = {} @@ -144,7 +155,6 @@ def test_populate_entry_span_data( "lambda.trigger": "aws:api.gateway", "http.host": "localhost", "http.url": "https://www.instana.com", - }, { "lambda.arn": "test", @@ -169,14 +179,13 @@ def test_populate_entry_span_data( ], ) def test_populate_entry_span_data_AWSlambda( - span_context: SpanContext, - attributes: Dict[str, Any] + span_context: SpanContext, span_processor: StanRecorder, attributes: Dict[str, Any] ) -> None: span_name = "aws.lambda.entry" service_name = "lambda" expected_result = attributes.copy() - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) reg_span = RegisteredSpan(span, None, service_name) span.set_attributes(attributes) @@ -192,13 +201,26 @@ def test_populate_entry_span_data_AWSlambda( assert expected_result["http.url"] == reg_span.data["http"]["url"] elif expected_result["lambda.trigger"] == "aws:cloudwatch.events": - assert expected_result["lambda.cw.events.resources"] == reg_span.data["lambda"]["cw"]["events"]["resources"] + assert ( + expected_result["lambda.cw.events.resources"] + == reg_span.data["lambda"]["cw"]["events"]["resources"] + ) elif expected_result["lambda.trigger"] == "aws:cloudwatch.logs": - assert expected_result["lambda.cw.logs.group"] == reg_span.data["lambda"]["cw"]["logs"]["group"] + assert ( + expected_result["lambda.cw.logs.group"] + == reg_span.data["lambda"]["cw"]["logs"]["group"] + ) elif expected_result["lambda.trigger"] == "aws:s3": - assert expected_result["lambda.s3.events"] == reg_span.data["lambda"]["s3"]["events"] + assert ( + expected_result["lambda.s3.events"] + == reg_span.data["lambda"]["s3"]["events"] + ) elif expected_result["lambda.trigger"] == "aws:sqs": - assert expected_result["lambda.sqs.messages"] == reg_span.data["lambda"]["sqs"]["messages"] + assert ( + expected_result["lambda.sqs.messages"] + == reg_span.data["lambda"]["sqs"]["messages"] + ) + @pytest.mark.parametrize( "span_name, service_name, attributes", @@ -290,7 +312,7 @@ def test_populate_entry_span_data_AWSlambda( { "gcs.op": "produce", "gcs.projectId": "MY_PROJECT", - "gcs.accessId": "Can not tell you!", + "gcs.accessId": "Can not tell you!", }, ), ( @@ -299,18 +321,19 @@ def test_populate_entry_span_data_AWSlambda( { "gcps.op": "produce", "gcps.projid": "MY_PROJECT", - "gcps.top": "MY_SUBSCRIPTION_NAME", + "gcps.top": "MY_SUBSCRIPTION_NAME", }, ), ], ) def test_populate_exit_span_data( span_context: SpanContext, + span_processor: StanRecorder, span_name: str, service_name: str, - attributes: Dict[str, Any] + attributes: Dict[str, Any], ) -> None: - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) reg_span = RegisteredSpan(span, None, service_name) expected_result = {} @@ -345,14 +368,12 @@ def test_populate_exit_span_data( ], ) def test_populate_exit_span_data_boto3( - span_context: SpanContext, - attributes: Dict[str, Any] + span_context: SpanContext, span_processor: StanRecorder, attributes: Dict[str, Any] ) -> None: span_name = service_name = "boto3" expected_result = attributes.copy() - - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) reg_span = RegisteredSpan(span, None, service_name) # expected_result = {} @@ -371,10 +392,11 @@ def test_populate_exit_span_data_boto3( assert value == reg_span.data[service_name][attr] - -def test_populate_exit_span_data_log(span_context: SpanContext) -> None: +def test_populate_exit_span_data_log( + span_context: SpanContext, span_processor: StanRecorder +) -> None: span_name = service_name = "log" - span = InstanaSpan(span_name, span_context) + span = InstanaSpan(span_name, span_context, span_processor) reg_span = RegisteredSpan(span, None, service_name) excepted_text = "Houston, we have a problem!" @@ -398,8 +420,8 @@ def test_populate_exit_span_data_log(span_context: SpanContext) -> None: time.time_ns(), ), ] - - for (event_name, attributes, timestamp) in events: + + for event_name, attributes, timestamp in events: span.add_event(event_name, attributes, timestamp) reg_span._populate_exit_span_data(span) diff --git a/tests/test_span_sdk.py b/tests/test_span_sdk.py index c7fc8d98..175fc60e 100644 --- a/tests/test_span_sdk.py +++ b/tests/test_span_sdk.py @@ -4,11 +4,13 @@ import pytest -from instana.span import InstanaSpan, SDKSpan +from instana.recorder import StanRecorder +from instana.span.sdk_span import SDKSpan +from instana.span.span import InstanaSpan from instana.span_context import SpanContext -def test_sdkspan(span_context: SpanContext) -> None: +def test_sdkspan(span_context: SpanContext, span_processor: StanRecorder) -> None: span_name = "test-sdk-span" service_name = "test-sdk" attributes = { @@ -16,7 +18,7 @@ def test_sdkspan(span_context: SpanContext) -> None: "arguments": "--quiet", "return": "True", } - span = InstanaSpan(span_name, span_context, attributes=attributes) + span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) sdk_span = SDKSpan(span, None, service_name) expected_result = { @@ -63,13 +65,16 @@ def test_sdkspan(span_context: SpanContext) -> None: ) def test_sdkspan_get_span_kind( span_context: SpanContext, + span_processor: StanRecorder, span_kind: str, expected_result: Tuple[str, int], ) -> None: attributes = { "span.kind": span_kind, } - span = InstanaSpan("test-sdk-span", span_context, attributes=attributes) + span = InstanaSpan( + "test-sdk-span", span_context, span_processor, attributes=attributes + ) sdk_span = SDKSpan(span, None, "test") kind = sdk_span.get_span_kind(span) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index feae19cc..a6bc810e 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,41 +1,43 @@ # (c) Copyright IBM Corp. 2024 -from opentelemetry.trace import set_span_in_context -from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID import pytest - -from instana.span import InstanaSpan +from instana.agent.test import TestAgent +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler +from instana.span.span import InstanaSpan from instana.span_context import SpanContext from instana.tracer import InstanaTracer, InstanaTracerProvider +from opentelemetry.context.context import Context +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID -def test_tracer_defaults() -> None: - provider = InstanaTracerProvider() +def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) assert tracer.tracer_id > INVALID_SPAN_ID assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE - assert tracer.recorder == provider.recorder - assert tracer._sampler == provider.sampler - assert tracer._span_processor == provider._span_processor - assert tracer._propagators == provider._propagators + assert isinstance(tracer._sampler, InstanaSampler) + assert isinstance(tracer.span_processor, StanRecorder) + assert isinstance(tracer.exporter, TestAgent) + assert len(tracer._propagators) == 3 -def test_tracer_start_span(span) -> None: + +def test_tracer_start_span( + tracer_provider: InstanaTracerProvider, context: Context +) -> None: span_name = "test-span" - provider = InstanaTracerProvider() tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) - parent_context = set_span_in_context(span) - span = tracer.start_span(name=span_name, context=parent_context) + span = tracer.start_span(name=span_name, context=context) assert span assert isinstance(span, InstanaSpan) @@ -43,14 +45,13 @@ def test_tracer_start_span(span) -> None: assert not span.stack -def test_tracer_start_span_with_stack(span: InstanaSpan) -> None: +def test_tracer_start_span_with_stack(tracer_provider: InstanaTracerProvider) -> None: span_name = "log" - provider = InstanaTracerProvider() tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) span = tracer.start_span(name=span_name) @@ -66,31 +67,31 @@ def test_tracer_start_span_with_stack(span: InstanaSpan) -> None: assert "m" in stack_0.keys() -def test_tracer_start_span_Exception(mocker, span) -> None: +def test_tracer_start_span_Exception( + mocker, tracer_provider: InstanaTracerProvider, context: Context +) -> None: span_name = "test-span" - provider = InstanaTracerProvider() tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) - parent_context = set_span_in_context(span) - - mocker.patch("instana.span.InstanaSpan.get_span_context", return_value={"key": "value"}) + mocker.patch( + "instana.span.span.InstanaSpan.get_span_context", return_value={"key": "value"} + ) with pytest.raises(TypeError): - tracer.start_span(name=span_name, context=parent_context) + tracer.start_span(name=span_name, context=context) -def test_tracer_start_as_current_span() -> None: +def test_tracer_start_as_current_span(tracer_provider: InstanaTracerProvider) -> None: span_name = "test-span" - provider = InstanaTracerProvider() tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) with tracer.start_as_current_span(name=span_name) as span: assert span is not None @@ -98,13 +99,14 @@ def test_tracer_start_as_current_span() -> None: assert span.name == span_name -def test_tracer_create_span_context(span_context: SpanContext) -> None: - provider = InstanaTracerProvider() +def test_tracer_create_span_context( + span_context: SpanContext, tracer_provider: InstanaTracerProvider +) -> None: tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) new_span_context = tracer._create_span_context(span_context) @@ -113,13 +115,14 @@ def test_tracer_create_span_context(span_context: SpanContext) -> None: assert span_context.long_trace_id == new_span_context.long_trace_id -def test_tracer_add_stack_high_limit(span: InstanaSpan) -> None: - provider = InstanaTracerProvider() +def test_tracer_add_stack_high_limit( + span: InstanaSpan, tracer_provider: InstanaTracerProvider +) -> None: tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) tracer._add_stack(span, 50) @@ -133,13 +136,14 @@ def test_tracer_add_stack_high_limit(span: InstanaSpan) -> None: assert "m" in stack_0.keys() -def test_tracer_add_stack_low_limit(span: InstanaSpan) -> None: - provider = InstanaTracerProvider() +def test_tracer_add_stack_low_limit( + span: InstanaSpan, tracer_provider: InstanaTracerProvider +) -> None: tracer = InstanaTracer( - provider.sampler, - provider.recorder, - provider._span_processor, - provider._propagators, + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, ) tracer._add_stack(span, 5) diff --git a/tests/test_tracer_provider.py b/tests/test_tracer_provider.py index d5222fad..8977ced8 100644 --- a/tests/test_tracer_provider.py +++ b/tests/test_tracer_provider.py @@ -1,8 +1,5 @@ # (c) Copyright IBM Corp. 2024 -from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID -from pytest import LogCaptureFixture - from instana.agent.host import HostAgent from instana.agent.test import TestAgent from instana.propagators.binary_propagator import BinaryPropagator @@ -12,13 +9,15 @@ from instana.recorder import StanRecorder from instana.sampling import InstanaSampler from instana.tracer import InstanaTracer, InstanaTracerProvider +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID +from pytest import LogCaptureFixture def test_tracer_provider_defaults() -> None: provider = InstanaTracerProvider() assert isinstance(provider.sampler, InstanaSampler) - assert isinstance(provider.recorder, StanRecorder) - assert isinstance(provider._span_processor, HostAgent) + assert isinstance(provider._span_processor, StanRecorder) + assert isinstance(provider._exporter, HostAgent) assert len(provider._propagators) == 3 assert isinstance(provider._propagators[Format.HTTP_HEADERS], HTTPPropagator) assert isinstance(provider._propagators[Format.TEXT_MAP], TextPropagator) @@ -46,9 +45,13 @@ def test_tracer_provider_get_tracer_empty_instrumenting_module_name( assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE -def test_tracer_provider_add_span_processor() -> None: +def test_tracer_provider_add_span_processor(span_processor: StanRecorder) -> None: provider = InstanaTracerProvider() - assert isinstance(provider._span_processor, HostAgent) - - provider.add_span_processor(TestAgent()) - assert isinstance(provider._span_processor, TestAgent) + assert isinstance(provider._span_processor, StanRecorder) + assert isinstance(provider._span_processor.agent, HostAgent) + assert provider._span_processor.THREAD_NAME == "InstanaSpan Recorder" + + provider.add_span_processor(span_processor) + assert isinstance(provider._span_processor, StanRecorder) + assert isinstance(provider._span_processor.agent, TestAgent) + assert provider._span_processor.THREAD_NAME == "InstanaSpan Recorder Test" From b3ff7bda7a284b14c4b411f6038feae1916bbf99 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 19 Jul 2024 19:27:39 +0530 Subject: [PATCH 032/172] fix: add stack to readable_span Signed-off-by: Varsha GS --- src/instana/span/readable_span.py | 5 +++-- src/instana/span/span.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/instana/span/readable_span.py b/src/instana/span/readable_span.py index 529030ef..add4d143 100644 --- a/src/instana/span/readable_span.py +++ b/src/instana/span/readable_span.py @@ -1,7 +1,7 @@ # (c) Copyright IBM Corp. 2024 from time import time_ns -from typing import Optional, Sequence +from typing import Optional, Sequence, List from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types @@ -55,6 +55,7 @@ def __init__( attributes: types.Attributes = {}, events: Sequence[Event] = [], status: Optional[Status] = Status(StatusCode.UNSET), + stack: Optional[List] = None, ) -> None: self._name = name self._context = context @@ -65,7 +66,7 @@ def __init__( self._events = events self._parent_id = parent_id self._status = status - self.stack = None + self.stack = stack self.synthetic = False if context.synthetic: self.synthetic = True diff --git a/src/instana/span/span.py b/src/instana/span/span.py index 5ec1a5db..16baba24 100644 --- a/src/instana/span/span.py +++ b/src/instana/span/span.py @@ -186,6 +186,7 @@ def _readable_span(self) -> ReadableSpan: attributes=self.attributes, events=self.events, status=self.status, + stack=self.stack, # kind=self.kind, ) From 0743218f9bdf1c3ec4b6b0c3baad08ada2710008 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 19 Jul 2024 20:59:03 +0530 Subject: [PATCH 033/172] fix: calculate and add duration to readable_span Signed-off-by: Varsha GS --- src/instana/span/base_span.py | 3 +-- src/instana/span/readable_span.py | 6 +++++- src/instana/span/span.py | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/instana/span/base_span.py b/src/instana/span/base_span.py index 11d2b733..028a339f 100644 --- a/src/instana/span/base_span.py +++ b/src/instana/span/base_span.py @@ -27,7 +27,7 @@ def __init__(self, span: Type["Span"], source, **kwargs) -> None: self.s = span.context.span_id self.l = span.context.level self.ts = round(span.start_time / 10**6) - self.d = round(span.duration / 10**6) + self.d = round(span.duration / 10**6) if span.duration is not None else None self.f = source self.ec = span.attributes.pop("ec", None) self.data = DictionaryOfStan() @@ -117,4 +117,3 @@ def _convert_attribute_value(self, value): logger.debug(final_value, exc_info=True) return None return final_value - diff --git a/src/instana/span/readable_span.py b/src/instana/span/readable_span.py index add4d143..fc06353a 100644 --- a/src/instana/span/readable_span.py +++ b/src/instana/span/readable_span.py @@ -61,7 +61,11 @@ def __init__( self._context = context self._start_time = start_time or time_ns() self._end_time = end_time - self._duration = 0 + self._duration = ( + self._end_time - self._start_time + if self._start_time and self._end_time + else None + ) self._attributes = attributes if attributes else {} self._events = events self._parent_id = parent_id diff --git a/src/instana/span/span.py b/src/instana/span/span.py index 16baba24..d03207b9 100644 --- a/src/instana/span/span.py +++ b/src/instana/span/span.py @@ -193,7 +193,6 @@ def _readable_span(self) -> ReadableSpan: def end(self, end_time: Optional[int] = None) -> None: with self._lock: self._end_time = end_time if end_time is not None else time_ns() - self._duration = self._end_time - self._start_time self._span_processor.record_span(self._readable_span()) From 3340c320fabd5cd86a7e11900542b57b2fb20b29 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 19 Jul 2024 21:05:35 +0530 Subject: [PATCH 034/172] fix(tests): Adapt unit tests to span.duration changes Signed-off-by: Varsha GS --- tests/test_span.py | 7 ------- tests/test_span_base.py | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/test_span.py b/tests/test_span.py index 48abadcd..03d261cf 100644 --- a/tests/test_span.py +++ b/tests/test_span.py @@ -630,9 +630,6 @@ def test_span_end_default( assert span.end_time assert isinstance(span.end_time, int) - assert span.duration - assert isinstance(span.duration, int) - assert span.duration > 0 def test_span_end(span_context: SpanContext, span_processor: StanRecorder) -> None: @@ -646,10 +643,6 @@ def test_span_end(span_context: SpanContext, span_processor: StanRecorder) -> No assert span.end_time assert span.end_time == timestamp_end - assert span.duration - assert isinstance(span.duration, int) - assert span.duration > 0 - assert span.duration == (timestamp_end - span.start_time) def test_span_mark_as_errored_default( diff --git a/tests/test_span_base.py b/tests/test_span_base.py index 75c88491..5f1a16c4 100644 --- a/tests/test_span_base.py +++ b/tests/test_span_base.py @@ -22,7 +22,7 @@ def test_basespan( "s": span_id, "l": 1, "ts": round(span.start_time / 10**6), - "d": round(span.duration / 10**6), + "d": None, "f": None, "ec": None, "data": DictionaryOfStan(), From 1fa77555214d07314c89ba53de9d0960e6f355c6 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 19 Jul 2024 21:50:13 +0530 Subject: [PATCH 035/172] fix: span_context changes - start_span() and start_as_current_span() should receive SpanContext obj as span_context - extract() should return SpanContext obj as span_context Signed-off-by: Varsha GS --- src/instana/propagators/base_propagator.py | 22 +++++++++++----------- src/instana/tracer.py | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index 056ba80b..a6a1add3 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -14,10 +14,7 @@ from opentelemetry.trace import ( INVALID_SPAN_ID, INVALID_TRACE_ID, - NonRecordingSpan, - set_span_in_context, ) -from opentelemetry.context.context import Context # The carrier, typed here as CarrierT, can be a dict, a list, or a tuple. # Using the trace header as an example, it can be in the following forms @@ -180,12 +177,14 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar and trace_id != INVALID_TRACE_ID and span_id != INVALID_SPAN_ID ): - ctx.trace_id = trace_id[-16:] # only the last 16 chars - ctx.span_id = span_id[-16:] # only the last 16 chars + # ctx.trace_id = trace_id[-16:] # only the last 16 chars + # ctx.span_id = span_id[-16:] # only the last 16 chars + ctx.trace_id = trace_id + ctx.span_id = span_id ctx.synthetic = synthetic is not None - if len(trace_id) > 16: - ctx.long_trace_id = trace_id + # if len(trace_id) > 16: + ctx.long_trace_id = trace_id elif not disable_w3c_trace_context and traceparent and trace_id is None and span_id is None: _, tp_trace_id, tp_parent_id, _ = self._tp.get_traceparent_fields(traceparent) @@ -234,12 +233,14 @@ def extract_instana_headers(self, dc): trace_id = dc.get(self.LC_HEADER_KEY_T) or dc.get(self.ALT_LC_HEADER_KEY_T) or dc.get( self.B_HEADER_KEY_T) or dc.get(self.B_ALT_LC_HEADER_KEY_T) if trace_id: - trace_id = header_to_long_id(trace_id) + # trace_id = header_to_long_id(trace_id) + trace_id = int(trace_id) span_id = dc.get(self.LC_HEADER_KEY_S) or dc.get(self.ALT_LC_HEADER_KEY_S) or dc.get( self.B_HEADER_KEY_S) or dc.get(self.B_ALT_LC_HEADER_KEY_S) if span_id: - span_id = header_to_id(span_id) + # span_id = header_to_id(span_id) + span_id = int(span_id) level = dc.get(self.LC_HEADER_KEY_L) or dc.get(self.ALT_LC_HEADER_KEY_L) or dc.get( self.B_HEADER_KEY_L) or dc.get(self.B_ALT_LC_HEADER_KEY_L) @@ -317,8 +318,7 @@ def extract(self, carrier, disable_w3c_trace_context=False): tracestate, disable_w3c_trace_context, ) - ctx = set_span_in_context(NonRecordingSpan(span_context), Context()) - return ctx + return span_context except Exception: logger.debug("extract error:", exc_info=True) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 60f9eb30..852f219b 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -114,7 +114,7 @@ def exporter(self) -> Optional[Union[HostAgent, TestAgent]]: def start_span( self, name: str, - context: Optional[Context] = None, + span_context: Optional[SpanContext] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, @@ -122,7 +122,7 @@ def start_span( record_exception: bool = True, set_status_on_exception: bool = True, ) -> InstanaSpan: - parent_context = get_current_span(context).get_span_context() + parent_context = span_context if parent_context is not None and not isinstance(parent_context, SpanContext): raise TypeError("parent_context must be an Instana SpanContext or None.") @@ -154,7 +154,7 @@ def start_span( def start_as_current_span( self, name: str, - context: Optional[Context] = None, + span_context: Optional[SpanContext] = None, kind: SpanKind = SpanKind.INTERNAL, attributes: types.Attributes = None, links: _Links = None, @@ -165,7 +165,7 @@ def start_as_current_span( ) -> Iterator[InstanaSpan]: span = self.start_span( name=name, - context=context, + span_context=span_context, kind=kind, attributes=attributes, links=links, From 572c0f68e3f201213dbe83dedea3177ee47c6be8 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 19 Jul 2024 21:50:48 +0530 Subject: [PATCH 036/172] fix(tests): Adapt unittests to span context changes Signed-off-by: Varsha GS --- tests/test_tracer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index a6bc810e..76ac7791 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -7,7 +7,6 @@ from instana.span.span import InstanaSpan from instana.span_context import SpanContext from instana.tracer import InstanaTracer, InstanaTracerProvider -from opentelemetry.context.context import Context from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID @@ -28,7 +27,7 @@ def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: def test_tracer_start_span( - tracer_provider: InstanaTracerProvider, context: Context + tracer_provider: InstanaTracerProvider, span_context: SpanContext ) -> None: span_name = "test-span" tracer = InstanaTracer( @@ -37,7 +36,7 @@ def test_tracer_start_span( tracer_provider._exporter, tracer_provider._propagators, ) - span = tracer.start_span(name=span_name, context=context) + span = tracer.start_span(name=span_name, span_context=span_context) assert span assert isinstance(span, InstanaSpan) @@ -68,7 +67,7 @@ def test_tracer_start_span_with_stack(tracer_provider: InstanaTracerProvider) -> def test_tracer_start_span_Exception( - mocker, tracer_provider: InstanaTracerProvider, context: Context + mocker, tracer_provider: InstanaTracerProvider, span_context: SpanContext ) -> None: span_name = "test-span" tracer = InstanaTracer( @@ -79,10 +78,11 @@ def test_tracer_start_span_Exception( ) mocker.patch( - "instana.span.span.InstanaSpan.get_span_context", return_value={"key": "value"} + "instana.tracer.InstanaTracer._create_span_context", + return_value={"key": "value"}, ) - with pytest.raises(TypeError): - tracer.start_span(name=span_name, context=context) + with pytest.raises(AttributeError): + tracer.start_span(name=span_name, span_context=span_context) def test_tracer_start_as_current_span(tracer_provider: InstanaTracerProvider) -> None: From fcbeb19607d448fa0ac05cf483fd81d4bda3a84d Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Sun, 21 Jul 2024 14:11:25 +0200 Subject: [PATCH 037/172] style: Update type hints. Use Type["BaseAgent"] instead of a long Union list with all its inherited classes. Update the return type hint for the Span duration property. Signed-off-by: Paulo Vital --- src/instana/span/readable_span.py | 2 +- src/instana/tracer.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/instana/span/readable_span.py b/src/instana/span/readable_span.py index fc06353a..3e95ec67 100644 --- a/src/instana/span/readable_span.py +++ b/src/instana/span/readable_span.py @@ -92,7 +92,7 @@ def end_time(self) -> Optional[int]: return self._end_time @property - def duration(self) -> int: + def duration(self) -> Optional[int]: return self._duration @property diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 852f219b..aee0b8ff 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -7,7 +7,7 @@ import time import traceback from contextlib import contextmanager -from typing import Iterator, Mapping, Optional, Union +from typing import TYPE_CHECKING, Iterator, Mapping, Optional, Type, Union from opentelemetry.context.context import Context from opentelemetry.trace import ( @@ -21,9 +21,7 @@ from opentelemetry.util import types from instana.agent.host import HostAgent -from instana.agent.test import TestAgent from instana.log import logger -from instana.propagators.base_propagator import CarrierT from instana.propagators.binary_propagator import BinaryPropagator from instana.propagators.exceptions import UnsupportedFormatException from instana.propagators.format import Format @@ -32,17 +30,21 @@ from instana.recorder import StanRecorder from instana.sampling import InstanaSampler, Sampler from instana.span.kind import EXIT_SPANS -from instana.span.span import InstanaSpan, get_current_span +from instana.span.span import InstanaSpan from instana.span_context import SpanContext from instana.util.ids import generate_id +if TYPE_CHECKING: + from instana.agent.base import BaseAgent + from instana.propagators.base_propagator import BasePropagator, CarrierT + class InstanaTracerProvider(TracerProvider): def __init__( self, sampler: Optional[Sampler] = None, span_processor: Optional[StanRecorder] = None, - exporter: Optional[Union[HostAgent, TestAgent]] = None, + exporter: Optional[Type["BaseAgent"]] = None, ) -> None: self.sampler = sampler or InstanaSampler() self._span_processor = span_processor or StanRecorder() @@ -88,10 +90,8 @@ def __init__( self, sampler: Sampler, span_processor: StanRecorder, - exporter: Union[HostAgent, TestAgent], - propagators: Mapping[ - str, Union[BinaryPropagator, HTTPPropagator, TextPropagator] - ], + exporter: Type["BaseAgent"], + propagators: Mapping[str, Type["BasePropagator"]], ) -> None: self._tracer_id = generate_id() self._sampler = sampler @@ -108,7 +108,7 @@ def span_processor(self) -> Optional[StanRecorder]: return self._span_processor @property - def exporter(self) -> Optional[Union[HostAgent, TestAgent]]: + def exporter(self) -> Optional[Type["BaseAgent"]]: return self._exporter def start_span( @@ -252,9 +252,9 @@ def inject( self, span_context: SpanContext, format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], - carrier: CarrierT, + carrier: "CarrierT", disable_w3c_trace_context: bool = False, - ) -> Optional[CarrierT]: + ) -> Optional["CarrierT"]: if format in self._propagators: return self._propagators[format].inject( span_context, carrier, disable_w3c_trace_context @@ -265,7 +265,7 @@ def inject( def extract( self, format: Union[Format.BINARY, Format.HTTP_HEADERS, Format.TEXT_MAP], - carrier: CarrierT, + carrier: "CarrierT", disable_w3c_trace_context: bool = False, ) -> Optional[Context]: if format in self._propagators: From fd651da1c4e06e350c2ea29e70cef12a448b7338 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Sun, 21 Jul 2024 14:13:53 +0200 Subject: [PATCH 038/172] fix(tests): Add Span duration unit tests. Add new tests to cover the changes on (readable)span duration property. Signed-off-by: Paulo Vital --- src/instana/span/base_span.py | 2 +- src/instana/span/span.py | 3 ++- tests/test_span.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/instana/span/base_span.py b/src/instana/span/base_span.py index 028a339f..1ff47b4d 100644 --- a/src/instana/span/base_span.py +++ b/src/instana/span/base_span.py @@ -27,7 +27,7 @@ def __init__(self, span: Type["Span"], source, **kwargs) -> None: self.s = span.context.span_id self.l = span.context.level self.ts = round(span.start_time / 10**6) - self.d = round(span.duration / 10**6) if span.duration is not None else None + self.d = round(span.duration / 10**6) if span.duration else None self.f = source self.ec = span.attributes.pop("ec", None) self.data = DictionaryOfStan() diff --git a/src/instana/span/span.py b/src/instana/span/span.py index d03207b9..61b99e55 100644 --- a/src/instana/span/span.py +++ b/src/instana/span/span.py @@ -192,7 +192,8 @@ def _readable_span(self) -> ReadableSpan: def end(self, end_time: Optional[int] = None) -> None: with self._lock: - self._end_time = end_time if end_time is not None else time_ns() + self._end_time = end_time if end_time else time_ns() + self._duration = self._end_time - self._start_time self._span_processor.record_span(self._readable_span()) diff --git a/tests/test_span.py b/tests/test_span.py index 03d261cf..2b3778dc 100644 --- a/tests/test_span.py +++ b/tests/test_span.py @@ -770,3 +770,38 @@ def test_get_current_span_INVALID_SPAN() -> None: assert span assert span == INVALID_SPAN + + +def test_span_duration_default( + span_context: SpanContext, span_processor: StanRecorder +) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.end_time + assert not span.duration + + span.end() + + assert span.end_time + assert span.duration + assert isinstance(span.duration, int) + assert span.duration > 0 + + +def test_span_duration(span_context: SpanContext, span_processor: StanRecorder) -> None: + span_name = "test-span" + span = InstanaSpan(span_name, span_context, span_processor) + + assert not span.end_time + assert not span.duration + + timestamp_end = time.time_ns() + span.end(timestamp_end) + + assert span.end_time + assert span.end_time == timestamp_end + assert span.duration + assert isinstance(span.duration, int) + assert span.duration > 0 + assert span.duration == (timestamp_end - span.start_time) From e50cbf2df0b3a1c18daf9c1d6ba5ec56c8711e03 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 22 Jul 2024 13:28:39 +0530 Subject: [PATCH 039/172] fix: use the ENUM class for intermediate span as well Signed-off-by: Varsha GS --- src/instana/span/registered_span.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py index 0a6e1f56..60749294 100644 --- a/src/instana/span/registered_span.py +++ b/src/instana/span/registered_span.py @@ -23,7 +23,7 @@ def __init__(self, span, source, service_name, **kwargs) -> None: self.k = SpanKind.CLIENT # exit self._populate_exit_span_data(span) elif span.name in LOCAL_SPANS: - self.k = 3 # intermediate span + self.k = SpanKind.INTERNAL # intermediate span self._populate_local_span_data(span) if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": From 0b3a57289a6097702c6566b4d93496621fd761f0 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 22 Jul 2024 13:29:55 +0530 Subject: [PATCH 040/172] fix(tests): Adapt registered_span unit tests after span.k refactor Signed-off-by: Varsha GS --- tests/test_span_registered.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_span_registered.py b/tests/test_span_registered.py index 9c111ca7..767669c3 100644 --- a/tests/test_span_registered.py +++ b/tests/test_span_registered.py @@ -20,7 +20,7 @@ ("gcps-producer", ("gcps", SpanKind.CLIENT, "gcps"), {}), ("urllib3", ("urllib3", SpanKind.CLIENT, "http"), {}), ("rabbitmq", ("rabbitmq", SpanKind.CLIENT, "rabbitmq"), {"sort": "publish"}), - ("render", ("render", 3, "render"), {"arguments": "--quiet"}), + ("render", ("render", SpanKind.INTERNAL, "render"), {"arguments": "--quiet"}), ], ) def test_registered_span( From 0184d0c1873db91a09380ac45afa15240a1a0df2 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 22 Jul 2024 13:30:33 +0530 Subject: [PATCH 041/172] fix: Report data to Agent Signed-off-by: Varsha GS --- src/instana/collector/aws_eks_fargate.py | 4 ++-- src/instana/collector/aws_fargate.py | 4 ++-- src/instana/collector/aws_lambda.py | 4 ++-- src/instana/collector/google_cloud_run.py | 4 ++-- src/instana/collector/host.py | 4 ++-- src/instana/collector/utils.py | 7 +++++-- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/instana/collector/aws_eks_fargate.py b/src/instana/collector/aws_eks_fargate.py index dac335e9..9b0fd3c0 100644 --- a/src/instana/collector/aws_eks_fargate.py +++ b/src/instana/collector/aws_eks_fargate.py @@ -9,7 +9,7 @@ from instana.collector.base import BaseCollector from instana.collector.helpers.eks.process import EKSFargateProcessHelper from instana.collector.helpers.runtime import RuntimeHelper -from instana.collector.utils import format_trace_and_span_ids +from instana.collector.utils import format_span from instana.log import logger from instana.util import DictionaryOfStan @@ -37,7 +37,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = format_trace_and_span_ids(self.queued_spans()) + payload["spans"] = format_span(self.queued_spans()) with_snapshot = self.should_send_snapshot_data() diff --git a/src/instana/collector/aws_fargate.py b/src/instana/collector/aws_fargate.py index d3168d32..a8bc7e0a 100644 --- a/src/instana/collector/aws_fargate.py +++ b/src/instana/collector/aws_fargate.py @@ -17,7 +17,7 @@ from instana.collector.helpers.fargate.process import FargateProcessHelper from instana.collector.helpers.fargate.task import TaskHelper from instana.collector.helpers.runtime import RuntimeHelper -from instana.collector.utils import format_trace_and_span_ids +from instana.collector.utils import format_span from instana.log import logger from instana.singletons import env_is_test from instana.util import DictionaryOfStan, validate_url @@ -145,7 +145,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = format_trace_and_span_ids(self.queued_spans()) + payload["spans"] = format_span(self.queued_spans()) with_snapshot = self.should_send_snapshot_data() diff --git a/src/instana/collector/aws_lambda.py b/src/instana/collector/aws_lambda.py index c0e4b731..1d680739 100644 --- a/src/instana/collector/aws_lambda.py +++ b/src/instana/collector/aws_lambda.py @@ -6,7 +6,7 @@ """ from instana.collector.base import BaseCollector -from instana.collector.utils import format_trace_and_span_ids +from instana.collector.utils import format_span from instana.log import logger from instana.util import DictionaryOfStan from instana.util.aws import normalize_aws_lambda_arn @@ -50,7 +50,7 @@ def prepare_payload(self): payload["metrics"] = None if not self.span_queue.empty(): - payload["spans"] = format_trace_and_span_ids(self.queued_spans()) + payload["spans"] = format_span(self.queued_spans()) if self.should_send_snapshot_data(): payload["metrics"] = self.snapshot_data diff --git a/src/instana/collector/google_cloud_run.py b/src/instana/collector/google_cloud_run.py index ffcbd984..65fdad02 100644 --- a/src/instana/collector/google_cloud_run.py +++ b/src/instana/collector/google_cloud_run.py @@ -15,7 +15,7 @@ InstanceEntityHelper, ) from instana.collector.helpers.google_cloud_run.process import GCRProcessHelper -from instana.collector.utils import format_trace_and_span_ids +from instana.collector.utils import format_span from instana.log import logger from instana.util import DictionaryOfStan, validate_url @@ -120,7 +120,7 @@ def prepare_payload(self): try: if not self.span_queue.empty(): - payload["spans"] = format_trace_and_span_ids(self.queued_spans()) + payload["spans"] = format_span(self.queued_spans()) self.fetching_start_time = int(time()) delta = self.fetching_start_time - self.__last_gcr_md_full_fetch diff --git a/src/instana/collector/host.py b/src/instana/collector/host.py index d5e3b77f..dfb2aacd 100644 --- a/src/instana/collector/host.py +++ b/src/instana/collector/host.py @@ -10,7 +10,7 @@ from instana.collector.base import BaseCollector from instana.collector.helpers.runtime import RuntimeHelper -from instana.collector.utils import format_trace_and_span_ids +from instana.collector.utils import format_span from instana.log import logger from instana.util import DictionaryOfStan @@ -81,7 +81,7 @@ def prepare_payload(self) -> DefaultDict[Any, Any]: try: if not self.span_queue.empty(): - payload["spans"] = format_trace_and_span_ids(self.queued_spans()) + payload["spans"] = format_span(self.queued_spans()) if not self.profile_queue.empty(): payload["profiles"] = self.queued_profiles() diff --git a/src/instana/collector/utils.py b/src/instana/collector/utils.py index a3dbc209..70c0f482 100644 --- a/src/instana/collector/utils.py +++ b/src/instana/collector/utils.py @@ -3,16 +3,17 @@ from typing import TYPE_CHECKING, List from opentelemetry.trace.span import format_span_id +from opentelemetry.trace import SpanKind if TYPE_CHECKING: from instana.span.span import InstanaSpan -def format_trace_and_span_ids( +def format_span( queued_spans: List["InstanaSpan"], ) -> List["InstanaSpan"]: """ - Format the Trace, Parent Span, and Span IDs of Spans to be a 64-bit + Format Span Kind and the Trace, Parent Span and Span IDs of the Spans to be a 64-bit Hexadecimal String instead of Integer before being pushed to a Collector (or Instana Agent). """ @@ -21,5 +22,7 @@ def format_trace_and_span_ids( span.t = format_span_id(span.t) span.s = format_span_id(span.s) span.p = format_span_id(span.p) if span.p else None + if isinstance(span.k, SpanKind): + span.k = span.k.value if not span.k is SpanKind.INTERNAL else 3 spans.append(span) return spans From 4658f07d9d3b60f00ef3e96376e33afebe34521c Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 22 Jul 2024 20:55:12 +0530 Subject: [PATCH 042/172] minor fixes Signed-off-by: Varsha GS --- src/instana/collector/utils.py | 8 ++++---- src/instana/span/registered_span.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/instana/collector/utils.py b/src/instana/collector/utils.py index 70c0f482..7292cca4 100644 --- a/src/instana/collector/utils.py +++ b/src/instana/collector/utils.py @@ -1,17 +1,17 @@ # (c) Copyright IBM Corp. 2024 -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Type, List from opentelemetry.trace.span import format_span_id from opentelemetry.trace import SpanKind if TYPE_CHECKING: - from instana.span.span import InstanaSpan + from instana.span.base_span import BaseSpan def format_span( - queued_spans: List["InstanaSpan"], -) -> List["InstanaSpan"]: + queued_spans: List[Type["BaseSpan"]], +) -> List[Type["BaseSpan"]]: """ Format Span Kind and the Trace, Parent Span and Span IDs of the Spans to be a 64-bit Hexadecimal String instead of Integer before being pushed to a diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py index 60749294..e92c4cd0 100644 --- a/src/instana/span/registered_span.py +++ b/src/instana/span/registered_span.py @@ -12,7 +12,9 @@ def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name super(RegisteredSpan, self).__init__(span, source, **kwargs) self.n = span.name - self.k = SpanKind.SERVER # entry + self.k = ( + SpanKind.SERVER + ) # entry -> Server span represents a synchronous incoming remote call such as an incoming HTTP request self.data["service"] = service_name if span.name in ENTRY_SPANS: @@ -20,10 +22,14 @@ def __init__(self, span, source, service_name, **kwargs) -> None: self._populate_entry_span_data(span) self._populate_extra_span_attributes(span) elif span.name in EXIT_SPANS: - self.k = SpanKind.CLIENT # exit + self.k = ( + SpanKind.CLIENT + ) # exit -> Client span represents a synchronous outgoing remote call such as an outgoing HTTP request or database call self._populate_exit_span_data(span) elif span.name in LOCAL_SPANS: - self.k = SpanKind.INTERNAL # intermediate span + self.k = ( + SpanKind.INTERNAL + ) # intermediate -> Internal span represents an internal operation within an application self._populate_local_span_data(span) if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": From 2fd9f1e76647c7effddabb498a4a79767da2eb77 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 15 Jul 2024 20:33:01 +0530 Subject: [PATCH 043/172] instrumentation(logging): Adapt to OTel spec Signed-off-by: Varsha GS --- src/instana/instrumentation/logging.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/instana/instrumentation/logging.py b/src/instana/instrumentation/logging.py index 77d11051..d49cc423 100644 --- a/src/instana/instrumentation/logging.py +++ b/src/instana/instrumentation/logging.py @@ -7,8 +7,10 @@ import logging from collections.abc import Mapping -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from opentelemetry.trace import set_span_in_context + +from instana.log import logger +from instana.util.traceutils import get_tracer_tuple, tracing_is_off @wrapt.patch_function_wrapper('logging', 'Logger._log') @@ -41,19 +43,22 @@ def log_with_instana(wrapped, instance, argv, kwargs): if t is not None and v is not None: parameters = '{} {}'.format(t , v) + parent_context = set_span_in_context(parent_span) + # create logging span - with tracer.start_active_span('log', child_of=parent_span) as scope: - scope.span.log_kv({ 'message': msg }) + with tracer.start_as_current_span("log", context=parent_context) as span: + event_attributes = {"message": msg} if parameters is not None: - scope.span.log_kv({ 'parameters': parameters }) + event_attributes.update({"parameters": parameters}) + span.add_event(name="log_with_instana", attributes=event_attributes) # extra tags for an error if argv[0] >= logging.ERROR: - scope.span.mark_as_errored() + span.mark_as_errored() + except Exception: logger.debug('log_with_instana:', exc_info=True) return wrapped(*argv, **kwargs, stacklevel=stacklevel) -logger.debug('Instrumenting logging') - +logger.debug("Instrumenting logging") From cb47e4ab2e91cfb0428289dc980a1feec086b249 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 15 Jul 2024 20:40:30 +0530 Subject: [PATCH 044/172] test(logging): Adapt unit tests after logging instrumentation refactor Signed-off-by: Varsha GS --- tests/clients/test_logging.py | 37 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 923b3f38..98030381 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -8,49 +8,50 @@ class TestLogging(unittest.TestCase): + @pytest.fixture def capture_log(self, caplog): self.caplog = caplog - def setUp(self): + def setUp(self) -> None: """ Clear all spans before a test run """ - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() self.logger = logging.getLogger('unit test') - def tearDown(self): + def tearDown(self) -> None: """ Ensure that allow_exit_as_root has the default value """ agent.options.allow_exit_as_root = False - def test_no_span(self): - with tracer.start_active_span('test'): + def test_no_span(self) -> None: + with tracer.start_as_current_span("test"): self.logger.info('info message') spans = self.recorder.queued_spans() self.assertEqual(1, len(spans)) - def test_extra_span(self): - with tracer.start_active_span('test'): - self.logger.warning('foo %s', 'bar') + def test_extra_span(self) -> None: + with tracer.start_as_current_span("test"): + self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() self.assertEqual(2, len(spans)) self.assertEqual(2, spans[0].k) - self.assertEqual('foo bar', spans[0].data["log"].get('message')) + self.assertEqual("foo bar", spans[0].data["event"].get("message")) - def test_log_with_tuple(self): - with tracer.start_active_span('test'): + def test_log_with_tuple(self) -> None: + with tracer.start_as_current_span("test"): self.logger.warning('foo %s', ("bar",)) spans = self.recorder.queued_spans() self.assertEqual(2, len(spans)) self.assertEqual(2, spans[0].k) - self.assertEqual("foo ('bar',)", spans[0].data["log"].get('message')) + self.assertEqual("foo ('bar',)", spans[0].data["event"].get("message")) - def test_parameters(self): - with tracer.start_active_span('test'): + def test_parameters(self) -> None: + with tracer.start_as_current_span("test"): try: a = 42 b = 0 @@ -61,16 +62,16 @@ def test_parameters(self): spans = self.recorder.queued_spans() self.assertEqual(2, len(spans)) - self.assertIsNotNone(spans[0].data["log"].get('parameters')) + self.assertIsNotNone(spans[0].data["event"].get("parameters")) - def test_no_root_exit_span(self): + def test_no_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.logger.info('info message') spans = self.recorder.queued_spans() self.assertEqual(0, len(spans)) - def test_root_exit_span(self): + def test_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.logger.warning('foo %s', 'bar') @@ -78,7 +79,7 @@ def test_root_exit_span(self): self.assertEqual(1, len(spans)) self.assertEqual(2, spans[0].k) - self.assertEqual('foo bar', spans[0].data["log"].get('message')) + self.assertEqual("foo bar", spans[0].data["event"].get("message")) @pytest.mark.usefixtures("capture_log") def test_log_caller(self): From c7c55861dc03c4890b5e52a3f2b5180459b1337c Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 23 Jul 2024 14:45:13 +0530 Subject: [PATCH 045/172] fix(logging): refactor instrumantation after span context changes Signed-off-by: Varsha GS --- src/instana/instrumentation/logging.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/instana/instrumentation/logging.py b/src/instana/instrumentation/logging.py index d49cc423..e061da62 100644 --- a/src/instana/instrumentation/logging.py +++ b/src/instana/instrumentation/logging.py @@ -7,13 +7,11 @@ import logging from collections.abc import Mapping -from opentelemetry.trace import set_span_in_context - from instana.log import logger from instana.util.traceutils import get_tracer_tuple, tracing_is_off -@wrapt.patch_function_wrapper('logging', 'Logger._log') +@wrapt.patch_function_wrapper("logging", "Logger._log") def log_with_instana(wrapped, instance, argv, kwargs): # argv[0] = level # argv[1] = message @@ -41,12 +39,12 @@ def log_with_instana(wrapped, instance, argv, kwargs): parameters = None (t, v, tb) = sys.exc_info() if t is not None and v is not None: - parameters = '{} {}'.format(t , v) + parameters = "{} {}".format(t, v) - parent_context = set_span_in_context(parent_span) + parent_context = parent_span.get_span_context() if parent_span else None # create logging span - with tracer.start_as_current_span("log", context=parent_context) as span: + with tracer.start_as_current_span("log", span_context=parent_context) as span: event_attributes = {"message": msg} if parameters is not None: event_attributes.update({"parameters": parameters}) @@ -56,7 +54,7 @@ def log_with_instana(wrapped, instance, argv, kwargs): span.mark_as_errored() except Exception: - logger.debug('log_with_instana:', exc_info=True) + logger.debug("log_with_instana:", exc_info=True) return wrapped(*argv, **kwargs, stacklevel=stacklevel) From 475e4c0cc83424fb89eb9c2db996cab0f89ce23e Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 23 Jul 2024 14:48:51 +0530 Subject: [PATCH 046/172] fix(tests): Adapt unit tests to span.kind changes Signed-off-by: Varsha GS --- tests/clients/test_logging.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 98030381..6019de1c 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -3,10 +3,12 @@ import logging import unittest + +from opentelemetry.trace import SpanKind + import pytest from instana.singletons import agent, tracer - class TestLogging(unittest.TestCase): @pytest.fixture @@ -14,18 +16,18 @@ def capture_log(self, caplog): self.caplog = caplog def setUp(self) -> None: - """ Clear all spans before a test run """ + """Clear all spans before a test run""" self.recorder = tracer.span_processor self.recorder.clear_spans() - self.logger = logging.getLogger('unit test') + self.logger = logging.getLogger("unit test") def tearDown(self) -> None: - """ Ensure that allow_exit_as_root has the default value """ + """Ensure that allow_exit_as_root has the default value""" agent.options.allow_exit_as_root = False def test_no_span(self) -> None: with tracer.start_as_current_span("test"): - self.logger.info('info message') + self.logger.info("info message") spans = self.recorder.queued_spans() self.assertEqual(1, len(spans)) @@ -36,17 +38,17 @@ def test_extra_span(self) -> None: spans = self.recorder.queued_spans() self.assertEqual(2, len(spans)) - self.assertEqual(2, spans[0].k) + self.assertIs(SpanKind.CLIENT, spans[0].k) self.assertEqual("foo bar", spans[0].data["event"].get("message")) def test_log_with_tuple(self) -> None: with tracer.start_as_current_span("test"): - self.logger.warning('foo %s', ("bar",)) + self.logger.warning("foo %s", ("bar",)) spans = self.recorder.queued_spans() self.assertEqual(2, len(spans)) - self.assertEqual(2, spans[0].k) + self.assertIs(SpanKind.CLIENT, spans[0].k) self.assertEqual("foo ('bar',)", spans[0].data["event"].get("message")) @@ -57,7 +59,7 @@ def test_parameters(self) -> None: b = 0 c = a / b except Exception as e: - self.logger.exception('Exception: %s', str(e)) + self.logger.exception("Exception: %s", str(e)) spans = self.recorder.queued_spans() self.assertEqual(2, len(spans)) @@ -66,18 +68,18 @@ def test_parameters(self) -> None: def test_no_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - self.logger.info('info message') + self.logger.info("info message") spans = self.recorder.queued_spans() self.assertEqual(0, len(spans)) def test_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - self.logger.warning('foo %s', 'bar') + self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() self.assertEqual(1, len(spans)) - self.assertEqual(2, spans[0].k) + self.assertIs(SpanKind.CLIENT, spans[0].k) self.assertEqual("foo bar", spans[0].data["event"].get("message")) From 9eb14c4115d80cb6785e8905bef6da47e32a3ee6 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 24 Jul 2024 18:54:23 +0530 Subject: [PATCH 047/172] tests(logging): Add tests to increase coverage Signed-off-by: Varsha GS --- tests/clients/test_logging.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 6019de1c..9ff06155 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -3,6 +3,7 @@ import logging import unittest +from unittest.mock import patch from opentelemetry.trace import SpanKind @@ -26,6 +27,7 @@ def tearDown(self) -> None: agent.options.allow_exit_as_root = False def test_no_span(self) -> None: + self.logger.setLevel(logging.INFO) with tracer.start_as_current_span("test"): self.logger.info("info message") @@ -52,6 +54,16 @@ def test_log_with_tuple(self) -> None: self.assertEqual("foo ('bar',)", spans[0].data["event"].get("message")) + def test_log_with_dict(self) -> None: + with tracer.start_as_current_span("test"): + self.logger.warning("foo %s", {"bar": 18}) + + spans = self.recorder.queued_spans() + self.assertEqual(2, len(spans)) + self.assertIs(SpanKind.CLIENT, spans[0].k) + + self.assertEqual("foo {'bar': 18}", spans[0].data["event"].get("message")) + def test_parameters(self) -> None: with tracer.start_as_current_span("test"): try: @@ -83,6 +95,20 @@ def test_root_exit_span(self) -> None: self.assertEqual("foo bar", spans[0].data["event"].get("message")) + def test_exception(self) -> None: + with tracer.start_as_current_span("test"): + with patch( + "instana.span.span.InstanaSpan.add_event", + side_effect=Exception("mocked error"), + ): + self.logger.warning("foo %s", "bar") + + spans = self.recorder.queued_spans() + self.assertEqual(2, len(spans)) + self.assertIs(SpanKind.CLIENT, spans[0].k) + + self.assertEqual({}, spans[0].data["event"]) + @pytest.mark.usefixtures("capture_log") def test_log_caller(self): handler = logging.StreamHandler() From daf3f1d235e8513e463caf74befa6c4a6c2a64ab Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 24 Jul 2024 19:30:09 +0530 Subject: [PATCH 048/172] tests(logging): use py std assert statements Signed-off-by: Varsha GS --- tests/clients/test_logging.py | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 9ff06155..d3329e02 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -32,37 +32,37 @@ def test_no_span(self) -> None: self.logger.info("info message") spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 def test_extra_span(self) -> None: with tracer.start_as_current_span("test"): self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIs(SpanKind.CLIENT, spans[0].k) + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT - self.assertEqual("foo bar", spans[0].data["event"].get("message")) + assert spans[0].data["event"].get("message") == "foo bar" def test_log_with_tuple(self) -> None: with tracer.start_as_current_span("test"): self.logger.warning("foo %s", ("bar",)) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIs(SpanKind.CLIENT, spans[0].k) + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT - self.assertEqual("foo ('bar',)", spans[0].data["event"].get("message")) + assert spans[0].data["event"].get("message") == "foo ('bar',)" def test_log_with_dict(self) -> None: with tracer.start_as_current_span("test"): self.logger.warning("foo %s", {"bar": 18}) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIs(SpanKind.CLIENT, spans[0].k) + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT - self.assertEqual("foo {'bar': 18}", spans[0].data["event"].get("message")) + assert spans[0].data["event"].get("message") == "foo {'bar': 18}" def test_parameters(self) -> None: with tracer.start_as_current_span("test"): @@ -74,26 +74,26 @@ def test_parameters(self) -> None: self.logger.exception("Exception: %s", str(e)) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - self.assertIsNotNone(spans[0].data["event"].get("parameters")) + assert spans[0].data["event"].get("parameters") is not None def test_no_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.logger.info("info message") spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 def test_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - self.assertIs(SpanKind.CLIENT, spans[0].k) + assert len(spans) == 1 + assert spans[0].k is SpanKind.CLIENT - self.assertEqual("foo bar", spans[0].data["event"].get("message")) + assert spans[0].data["event"].get("message") == "foo bar" def test_exception(self) -> None: with tracer.start_as_current_span("test"): @@ -104,10 +104,10 @@ def test_exception(self) -> None: self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertIs(SpanKind.CLIENT, spans[0].k) + assert len(spans) == 2 + assert spans[0].k is SpanKind.CLIENT - self.assertEqual({}, spans[0].data["event"]) + assert spans[0].data["event"] == {} @pytest.mark.usefixtures("capture_log") def test_log_caller(self): From cde4a93965315ad344e4027472f06456d0c25c5e Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 24 Jul 2024 19:34:37 +0530 Subject: [PATCH 049/172] fix(style): Add type hints Signed-off-by: Varsha GS --- src/instana/instrumentation/logging.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/instana/instrumentation/logging.py b/src/instana/instrumentation/logging.py index e061da62..0f2280a0 100644 --- a/src/instana/instrumentation/logging.py +++ b/src/instana/instrumentation/logging.py @@ -6,13 +6,19 @@ import wrapt import logging from collections.abc import Mapping +from typing import Any, Tuple, Dict, Callable from instana.log import logger from instana.util.traceutils import get_tracer_tuple, tracing_is_off @wrapt.patch_function_wrapper("logging", "Logger._log") -def log_with_instana(wrapped, instance, argv, kwargs): +def log_with_instana( + wrapped: Callable[..., None], + instance: logging.Logger, + argv: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], +) -> Callable[..., None]: # argv[0] = level # argv[1] = message # argv[2] = args for message From 25f6e3ad7378db9fbd69427de4c88c740b2aa4d9 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 25 Jul 2024 02:42:42 -0700 Subject: [PATCH 050/172] fix: TracerProvider.get_tracer() after API update. The new OTel API version 1.26.0 has introduced changes on the TracerProvider.get_tracer() which must be reflected on our code. Signed-off-by: Paulo Vital --- pyproject.toml | 2 +- src/instana/tracer.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1d225f3f..59617c46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ "requests>=2.6.0", "six>=1.12.0", "urllib3>=1.26.5", - "opentelemetry-api>=1.23.0", + "opentelemetry-api>=1.26.0", ] [project.entry-points."instana"] diff --git a/src/instana/tracer.py b/src/instana/tracer.py index aee0b8ff..d7fec79a 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -59,6 +59,7 @@ def get_tracer( instrumenting_module_name: str, instrumenting_library_version: Optional[str] = None, schema_url: Optional[str] = None, + attributes: Optional[types.Attributes] = None, ) -> Tracer: if not instrumenting_module_name: # Reject empty strings too. instrumenting_module_name = "" From 78477a1a33f7109b04b30301d14c2e09dbf5ba88 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 29 Jul 2024 20:31:35 +0530 Subject: [PATCH 051/172] fix: log message Signed-off-by: Varsha GS --- src/instana/span/registered_span.py | 10 ++++------ tests/clients/test_logging.py | 12 ++++++------ tests/test_span_registered.py | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py index e92c4cd0..e8175db0 100644 --- a/src/instana/span/registered_span.py +++ b/src/instana/span/registered_span.py @@ -139,8 +139,8 @@ def _populate_local_span_data(self, span) -> None: if span.name == "render": self.data["render"]["name"] = span.attributes.pop("name", None) self.data["render"]["type"] = span.attributes.pop("type", None) - self.data["event"]["message"] = span.attributes.pop("message", None) - self.data["event"]["parameters"] = span.attributes.pop("parameters", None) + self.data["log"]["message"] = span.attributes.pop("message", None) + self.data["log"]["parameters"] = span.attributes.pop("parameters", None) else: logger.debug("SpanRecorder: Unknown local span: %s" % span.name) @@ -309,11 +309,9 @@ def _populate_exit_span_data(self, span) -> None: # use last special key values for event in span.events: if "message" in event.attributes: - self.data["event"]["message"] = event.attributes.pop( - "message", None - ) + self.data["log"]["message"] = event.attributes.pop("message", None) if "parameters" in event.attributes: - self.data["event"]["parameters"] = event.attributes.pop( + self.data["log"]["parameters"] = event.attributes.pop( "parameters", None ) else: diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index d3329e02..9c2b223b 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -42,7 +42,7 @@ def test_extra_span(self) -> None: assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["event"].get("message") == "foo bar" + assert spans[0].data["log"].get("message") == "foo bar" def test_log_with_tuple(self) -> None: with tracer.start_as_current_span("test"): @@ -52,7 +52,7 @@ def test_log_with_tuple(self) -> None: assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["event"].get("message") == "foo ('bar',)" + assert spans[0].data["log"].get("message") == "foo ('bar',)" def test_log_with_dict(self) -> None: with tracer.start_as_current_span("test"): @@ -62,7 +62,7 @@ def test_log_with_dict(self) -> None: assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["event"].get("message") == "foo {'bar': 18}" + assert spans[0].data["log"].get("message") == "foo {'bar': 18}" def test_parameters(self) -> None: with tracer.start_as_current_span("test"): @@ -76,7 +76,7 @@ def test_parameters(self) -> None: spans = self.recorder.queued_spans() assert len(spans) == 2 - assert spans[0].data["event"].get("parameters") is not None + assert spans[0].data["log"].get("parameters") is not None def test_no_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True @@ -93,7 +93,7 @@ def test_root_exit_span(self) -> None: assert len(spans) == 1 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["event"].get("message") == "foo bar" + assert spans[0].data["log"].get("message") == "foo bar" def test_exception(self) -> None: with tracer.start_as_current_span("test"): @@ -107,7 +107,7 @@ def test_exception(self) -> None: assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["event"] == {} + assert spans[0].data["log"] == {} @pytest.mark.usefixtures("capture_log") def test_log_caller(self): diff --git a/tests/test_span_registered.py b/tests/test_span_registered.py index 767669c3..3ba7bc6d 100644 --- a/tests/test_span_registered.py +++ b/tests/test_span_registered.py @@ -426,5 +426,5 @@ def test_populate_exit_span_data_log( reg_span._populate_exit_span_data(span) - assert excepted_text == reg_span.data["event"]["message"] - assert excepted_text == reg_span.data["event"]["parameters"] + assert excepted_text == reg_span.data["log"]["message"] + assert excepted_text == reg_span.data["log"]["parameters"] From 5b62118a0da5561d834b2b797a119674b6cae551 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 30 Jul 2024 13:08:42 +0530 Subject: [PATCH 052/172] fix: different traces' trace id within a session Signed-off-by: Varsha GS --- src/instana/tracer.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index d7fec79a..266d5b7a 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -94,16 +94,11 @@ def __init__( exporter: Type["BaseAgent"], propagators: Mapping[str, Type["BasePropagator"]], ) -> None: - self._tracer_id = generate_id() self._sampler = sampler self._span_processor = span_processor self._exporter = exporter self._propagators = propagators - @property - def tracer_id(self) -> str: - return self._tracer_id - @property def span_processor(self) -> Optional[StanRecorder]: return self._span_processor @@ -218,14 +213,17 @@ def _add_stack(self, span: InstanaSpan, limit: Optional[int] = 30) -> None: def _create_span_context(self, parent_context: SpanContext) -> SpanContext: """Creates a new SpanContext based on the given parent context.""" + generated_id = generate_id() + if parent_context is not None and parent_context.trace_id is not None: trace_id = parent_context.trace_id - span_id = generate_id() + span_id = generated_id trace_flags = parent_context.trace_flags is_remote = parent_context.is_remote else: - trace_id = self.tracer_id - span_id = self.tracer_id + # root span + trace_id = generated_id + span_id = generated_id trace_flags = TraceFlags(self._sampler.sampled()) is_remote = False From 1e5076e669b1e43b42ec5c5257cc7df26c322842 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 30 Jul 2024 14:05:39 +0530 Subject: [PATCH 053/172] fix(tests): Remove tracer_id from tests Signed-off-by: Varsha GS --- tests/test_tracer.py | 2 -- tests/test_tracer_provider.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 76ac7791..de29aee6 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -18,8 +18,6 @@ def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: tracer_provider._propagators, ) - assert tracer.tracer_id > INVALID_SPAN_ID - assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE assert isinstance(tracer._sampler, InstanaSampler) assert isinstance(tracer.span_processor, StanRecorder) assert isinstance(tracer.exporter, TestAgent) diff --git a/tests/test_tracer_provider.py b/tests/test_tracer_provider.py index 8977ced8..b4b9f8bc 100644 --- a/tests/test_tracer_provider.py +++ b/tests/test_tracer_provider.py @@ -29,8 +29,6 @@ def test_tracer_provider_get_tracer() -> None: tracer = provider.get_tracer("instana.test.tracer") assert isinstance(tracer, InstanaTracer) - assert tracer.tracer_id > INVALID_SPAN_ID - assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE def test_tracer_provider_get_tracer_empty_instrumenting_module_name( @@ -41,8 +39,6 @@ def test_tracer_provider_get_tracer_empty_instrumenting_module_name( assert "get_tracer called with missing module name." == caplog.record_tuples[0][2] assert isinstance(tracer, InstanaTracer) - assert tracer.tracer_id > INVALID_SPAN_ID - assert tracer.tracer_id <= _SPAN_ID_MAX_VALUE def test_tracer_provider_add_span_processor(span_processor: StanRecorder) -> None: From 230963dbbb12eedcde850ab3c4c496bab9f4597e Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 30 Jul 2024 20:54:52 +0530 Subject: [PATCH 054/172] fix: Add tests for root span context Signed-off-by: Varsha GS --- src/instana/tracer.py | 10 +++------- tests/test_tracer.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 266d5b7a..f7ffdcfc 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -124,7 +124,7 @@ def start_span( raise TypeError("parent_context must be an Instana SpanContext or None.") if parent_context is not None and not parent_context.is_valid: - # We probably have a INVALID_SPAN_CONTEXT. + # We probably have an INVALID_SPAN_CONTEXT. parent_context = None span_context = self._create_span_context(parent_context) @@ -213,17 +213,13 @@ def _add_stack(self, span: InstanaSpan, limit: Optional[int] = 30) -> None: def _create_span_context(self, parent_context: SpanContext) -> SpanContext: """Creates a new SpanContext based on the given parent context.""" - generated_id = generate_id() - if parent_context is not None and parent_context.trace_id is not None: trace_id = parent_context.trace_id - span_id = generated_id + span_id = generate_id() trace_flags = parent_context.trace_flags is_remote = parent_context.is_remote else: - # root span - trace_id = generated_id - span_id = generated_id + trace_id = span_id = generate_id() trace_flags = TraceFlags(self._sampler.sampled()) is_remote = False diff --git a/tests/test_tracer.py b/tests/test_tracer.py index de29aee6..ba6ec778 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -112,6 +112,29 @@ def test_tracer_create_span_context( assert span_context.span_id != new_span_context.span_id assert span_context.long_trace_id == new_span_context.long_trace_id + assert span_context.trace_id > INVALID_SPAN_ID + assert span_context.trace_id <= _SPAN_ID_MAX_VALUE + + assert span_context.span_id > INVALID_SPAN_ID + assert span_context.span_id <= _SPAN_ID_MAX_VALUE + + +def test_tracer_create_span_context_root( + tracer_provider: InstanaTracerProvider, +) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + new_span_context = tracer._create_span_context(parent_context=None) + + assert new_span_context.trace_id > INVALID_SPAN_ID + assert new_span_context.trace_id <= _SPAN_ID_MAX_VALUE + + assert new_span_context.trace_id == new_span_context.span_id + def test_tracer_add_stack_high_limit( span: InstanaSpan, tracer_provider: InstanaTracerProvider From a1fc1480e350c6b1124b40fee7c445f584d1c6e2 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 4 Jul 2024 17:43:23 +0530 Subject: [PATCH 055/172] instrumentation(flask): add logic to set the implicit current context throughout the request Signed-off-by: Varsha GS (cherry picked from commit b8eddfd24cf75afbe7ab4007b73238104204a0a0) --- .../instrumentation/flask/with_blinker.py | 63 ++++++++++++------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index cac55c96..f3d6a77f 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -4,44 +4,53 @@ import re import wrapt -import opentracing -import opentracing.ext.tags as ext +from opentelemetry.semconv.trace import SpanAttributes as ext +from opentelemetry import context, trace from ...log import logger from ...util.secrets import strip_secrets_from_query from ...singletons import agent, tracer from .common import extract_custom_headers +from instana.propagators.format import Format import flask from flask import request_started, request_finished, got_request_exception -path_tpl_re = re.compile('<.*>') +path_tpl_re = re.compile("<.*>") def request_started_with_instana(sender, **extra): try: env = flask.request.environ - ctx = None ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env) - flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx) + flask.g.scope = tracer.start_active_span("wsgi", child_of=ctx) span = flask.g.scope.span + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + flask.g.token = token + extract_custom_headers(span, env, format=True) span.set_tag(ext.HTTP_METHOD, flask.request.method) - if 'PATH_INFO' in env: - span.set_tag(ext.HTTP_URL, env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) + if "PATH_INFO" in env: + span.set_tag(ext.HTTP_URL, env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) span.set_tag("http.params", scrubbed_params) - if 'HTTP_HOST' in env: - span.set_tag("http.host", env['HTTP_HOST']) + if "HTTP_HOST" in env: + span.set_tag("http.host", env["HTTP_HOST"]) - if hasattr(flask.request.url_rule, 'rule') and \ - path_tpl_re.search(flask.request.url_rule.rule) is not None: + if ( + hasattr(flask.request.url_rule, "rule") + and path_tpl_re.search(flask.request.url_rule.rule) is not None + ): path_tpl = flask.request.url_rule.rule.replace("<", "{") path_tpl = path_tpl.replace(">", "}") span.set_tag("http.path_tpl", path_tpl) @@ -52,7 +61,7 @@ def request_started_with_instana(sender, **extra): def request_finished_with_instana(sender, response, **extra): scope = None try: - if not hasattr(flask.g, 'scope'): + if not hasattr(flask.g, "scope"): return scope = flask.g.scope @@ -65,8 +74,12 @@ def request_finished_with_instana(sender, response, **extra): span.set_tag(ext.HTTP_STATUS_CODE, int(response.status_code)) extract_custom_headers(span, response.headers, format=False) - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - response.headers.add('Server-Timing', "intid;desc=%s" % scope.span.context.trace_id) + tracer.inject( + scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers + ) + response.headers.add( + "Server-Timing", "intid;desc=%s" % scope.span.context.trace_id + ) except: logger.debug("Flask after_request", exc_info=True) finally: @@ -75,7 +88,7 @@ def request_finished_with_instana(sender, response, **extra): def log_exception_with_instana(sender, exception, **extra): - if hasattr(flask.g, 'scope') and flask.g.scope is not None: + if hasattr(flask.g, "scope") and flask.g.scope is not None: scope = flask.g.scope if scope.span is not None: scope.span.log_exception(exception) @@ -93,7 +106,7 @@ def teardown_request_with_instana(*argv, **kwargs): In the case of exceptions, after_request_with_instana isn't called so we capture those cases here. """ - if hasattr(flask.g, 'scope') and flask.g.scope is not None: + if hasattr(flask.g, "scope") and flask.g.scope is not None: if len(argv) > 0 and argv[0] is not None: scope = flask.g.scope scope.span.log_exception(argv[0]) @@ -102,11 +115,17 @@ def teardown_request_with_instana(*argv, **kwargs): flask.g.scope.close() flask.g.scope = None + if hasattr(flask.g, "token") and flask.g.token is not None: + context.detach(flask.g.token) + flask.g.token = None + -@wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request') +@wrapt.patch_function_wrapper("flask", "Flask.full_dispatch_request") def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs): - if not hasattr(instance, '_stan_wuz_here'): - logger.debug("Flask(blinker): Applying flask before/after instrumentation funcs") + if not hasattr(instance, "_stan_wuz_here"): + logger.debug( + "Flask(blinker): Applying flask before/after instrumentation funcs" + ) setattr(instance, "_stan_wuz_here", True) got_request_exception.connect(log_exception_with_instana, instance) request_started.connect(request_started_with_instana, instance) From 785efa93571aa9b4bdca81a5d165c82814ead954 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 9 Jul 2024 13:23:52 +0530 Subject: [PATCH 056/172] instrumentation(flask): Adapt common to OTel spec Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/__init__.py | 12 +++--- src/instana/instrumentation/flask/common.py | 41 ++++++++++--------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/instana/instrumentation/flask/__init__.py b/src/instana/instrumentation/flask/__init__.py index 3ec4b3e9..07cbbd39 100644 --- a/src/instana/instrumentation/flask/__init__.py +++ b/src/instana/instrumentation/flask/__init__.py @@ -11,13 +11,13 @@ # Blinker support is preferred but we do the best we can when it's not available. # if hasattr(flask.signals, 'signals_available'): - from flask.signals import signals_available + from flask.signals import signals_available else: - # Beginning from 2.3.0 as stated in the notes - # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 - # "Signals are always available. blinker>=1.6.2 is a required dependency. - # The signals_available attribute is deprecated. #5056" - signals_available = True + # Beginning from 2.3.0 as stated in the notes + # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 + # "Signals are always available. blinker>=1.6.2 is a required dependency. + # The signals_available attribute is deprecated. #5056" + signals_available = True from . import common diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 58de6ae2..373cbd2b 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -4,35 +4,37 @@ import wrapt import flask -import opentracing -import opentracing.ext.tags as ext + +from opentelemetry.semconv.trace import SpanAttributes as ext +from opentelemetry.trace import set_span_in_context from ...log import logger from ...singletons import tracer, agent - +from instana.propagators.format import Format @wrapt.patch_function_wrapper('flask', 'templating._render') def render_with_instana(wrapped, instance, argv, kwargs): # If we're not tracing, just return - if not (hasattr(flask, 'g') and hasattr(flask.g, 'scope')): + if not (hasattr(flask, "g") and hasattr(flask.g, "span")): return wrapped(*argv, **kwargs) - parent_span = flask.g.scope.span + parent_span = flask.g.span + parent_context = set_span_in_context(parent_span) - with tracer.start_active_span("render", child_of=parent_span) as rscope: + with tracer.start_as_current_span("render", context=parent_context) as span: try: flask_version = tuple(map(int, flask.__version__.split('.'))) template = argv[1] if flask_version >= (2, 2, 0) else argv[0] - rscope.span.set_tag("type", "template") + span.set_attribute("type", "template") if template.name is None: - rscope.span.set_tag("name", '(from string)') + span.set_attribute("name", "(from string)") else: - rscope.span.set_tag("name", template.name) + span.set_attribute("name", template.name) return wrapped(*argv, **kwargs) except Exception as e: - rscope.span.log_exception(e) + span.record_exception(e) raise @@ -44,9 +46,8 @@ def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): try: exc = argv[0] - if hasattr(flask.g, 'scope') and flask.g.scope is not None: - scope = flask.g.scope - span = scope.span + if hasattr(flask.g, "span") and flask.g.span is not None: + span = flask.g.span if response is not None: if isinstance(response, tuple): @@ -60,18 +61,18 @@ def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): if 500 <= status_code: span.log_exception(exc) - span.set_tag(ext.HTTP_STATUS_CODE, int(status_code)) + span.set_attribute(ext.HTTP_STATUS_CODE, int(status_code)) if hasattr(response, 'headers'): - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - value = "intid;desc=%s" % scope.span.context.trace_id + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + value = "intid;desc=%s" % span.context.trace_id if hasattr(response.headers, 'add'): response.headers.add('Server-Timing', value) elif type(response.headers) is dict or hasattr(response.headers, "__dict__"): response.headers['Server-Timing'] = value - scope.close() - flask.g.scope = None + span.end() + flask.g.span = None except: logger.debug("handle_user_exception_with_instana:", exc_info=True) @@ -86,7 +87,9 @@ def extract_custom_headers(span, headers, format): # Headers are available in this format: HTTP_X_CAPTURE_THIS flask_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if format else custom_header if flask_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[flask_header]) + span.set_attribute( + "http.header.%s" % custom_header, headers[flask_header] + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) From f88db53c796fcb3fcfbcc270eb3b939bb7abbd1f Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 9 Jul 2024 13:24:19 +0530 Subject: [PATCH 057/172] instrumentation(flask): Adapt with_blinker to OTel spec Signed-off-by: Varsha GS --- .../instrumentation/flask/with_blinker.py | 62 +++++++++---------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index f3d6a77f..3f1d4412 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -23,10 +23,10 @@ def request_started_with_instana(sender, **extra): try: env = flask.request.environ - ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env) + ctx = tracer.extract(Format.HTTP_HEADERS, env) - flask.g.scope = tracer.start_active_span("wsgi", child_of=ctx) - span = flask.g.scope.span + span = tracer.start_span("wsgi", context=ctx) + flask.g.span = span ctx = trace.set_span_in_context(span) token = context.attach(ctx) @@ -34,18 +34,18 @@ def request_started_with_instana(sender, **extra): extract_custom_headers(span, env, format=True) - span.set_tag(ext.HTTP_METHOD, flask.request.method) + span.set_attribute(ext.HTTP_METHOD, flask.request.method) if "PATH_INFO" in env: - span.set_tag(ext.HTTP_URL, env["PATH_INFO"]) + span.set_attribute(ext.HTTP_URL, env["PATH_INFO"]) if "QUERY_STRING" in env and len(env["QUERY_STRING"]): scrubbed_params = strip_secrets_from_query( env["QUERY_STRING"], agent.options.secrets_matcher, agent.options.secrets_list, ) - span.set_tag("http.params", scrubbed_params) + span.set_attribute("http.params", scrubbed_params) if "HTTP_HOST" in env: - span.set_tag("http.host", env["HTTP_HOST"]) + span.set_attribute("http.host", env["HTTP_HOST"]) if ( hasattr(flask.request.url_rule, "rule") @@ -53,52 +53,48 @@ def request_started_with_instana(sender, **extra): ): path_tpl = flask.request.url_rule.rule.replace("<", "{") path_tpl = path_tpl.replace(">", "}") - span.set_tag("http.path_tpl", path_tpl) + span.set_attribute("http.path_tpl", path_tpl) except: logger.debug("Flask before_request", exc_info=True) def request_finished_with_instana(sender, response, **extra): - scope = None try: - if not hasattr(flask.g, "scope"): + if not hasattr(flask.g, "span"): return - scope = flask.g.scope - if scope is not None: - span = scope.span + span = flask.g.span + if span is not None: if 500 <= response.status_code: span.mark_as_errored() - span.set_tag(ext.HTTP_STATUS_CODE, int(response.status_code)) + span.set_attribute(ext.HTTP_STATUS_CODE, int(response.status_code)) extract_custom_headers(span, response.headers, format=False) - tracer.inject( - scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers - ) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) response.headers.add( - "Server-Timing", "intid;desc=%s" % scope.span.context.trace_id + "Server-Timing", "intid;desc=%s" % span.context.trace_id ) except: logger.debug("Flask after_request", exc_info=True) finally: - if scope is not None: - scope.close() + if span is not None: + span.end() def log_exception_with_instana(sender, exception, **extra): - if hasattr(flask.g, "scope") and flask.g.scope is not None: - scope = flask.g.scope - if scope.span is not None: - scope.span.log_exception(exception) + if hasattr(flask.g, "span") and flask.g.span is not None: + span = flask.g.span + if span is not None: + span.record_exception(exception) # As of Flask 2.3.x: # https://github.com/pallets/flask/blob/ # d0bf462866289ad8bfe29b6e4e1e0f531003ab34/src/flask/app.py#L1379 # The `got_request_exception` signal, is only sent by # the `handle_exception` method which "always causes a 500" - scope.span.set_tag(ext.HTTP_STATUS_CODE, 500) - scope.close() + span.set_attribute(ext.HTTP_STATUS_CODE, 500) + span.end() def teardown_request_with_instana(*argv, **kwargs): @@ -106,14 +102,14 @@ def teardown_request_with_instana(*argv, **kwargs): In the case of exceptions, after_request_with_instana isn't called so we capture those cases here. """ - if hasattr(flask.g, "scope") and flask.g.scope is not None: + if hasattr(flask.g, "span") and flask.g.span is not None: if len(argv) > 0 and argv[0] is not None: - scope = flask.g.scope - scope.span.log_exception(argv[0]) - if ext.HTTP_STATUS_CODE not in scope.span.tags: - scope.span.set_tag(ext.HTTP_STATUS_CODE, 500) - flask.g.scope.close() - flask.g.scope = None + span = flask.g.span + span.record_exception(argv[0]) + if ext.HTTP_STATUS_CODE not in span.attributes: + span.set_attribute(ext.HTTP_STATUS_CODE, 500) + flask.g.span.end() + flask.g.span = None if hasattr(flask.g, "token") and flask.g.token is not None: context.detach(flask.g.token) From 7edbd4198f33660c1f527a71358391680608d776 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 10 Jul 2024 15:49:05 +0530 Subject: [PATCH 058/172] instrumentation(flask): end the span only if it is recording Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/with_blinker.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index 3f1d4412..22343344 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -55,7 +55,7 @@ def request_started_with_instana(sender, **extra): path_tpl = path_tpl.replace(">", "}") span.set_attribute("http.path_tpl", path_tpl) except: - logger.debug("Flask before_request", exc_info=True) + logger.debug("Flask request_started_with_instana", exc_info=True) def request_finished_with_instana(sender, response, **extra): @@ -77,9 +77,9 @@ def request_finished_with_instana(sender, response, **extra): "Server-Timing", "intid;desc=%s" % span.context.trace_id ) except: - logger.debug("Flask after_request", exc_info=True) + logger.debug("Flask request_finished_with_instana", exc_info=True) finally: - if span is not None: + if span and span.is_recording(): span.end() @@ -94,12 +94,13 @@ def log_exception_with_instana(sender, exception, **extra): # The `got_request_exception` signal, is only sent by # the `handle_exception` method which "always causes a 500" span.set_attribute(ext.HTTP_STATUS_CODE, 500) - span.end() + if span.is_recording(): + span.end() def teardown_request_with_instana(*argv, **kwargs): """ - In the case of exceptions, after_request_with_instana isn't called + In the case of exceptions, request_finished_with_instana isn't called so we capture those cases here. """ if hasattr(flask.g, "span") and flask.g.span is not None: @@ -108,7 +109,8 @@ def teardown_request_with_instana(*argv, **kwargs): span.record_exception(argv[0]) if ext.HTTP_STATUS_CODE not in span.attributes: span.set_attribute(ext.HTTP_STATUS_CODE, 500) - flask.g.span.end() + if flask.g.span.is_recording(): + flask.g.span.end() flask.g.span = None if hasattr(flask.g, "token") and flask.g.token is not None: From 1c90756fa70ce2fa6e9fd454faf59abadf19bfc7 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 10 Jul 2024 15:52:09 +0530 Subject: [PATCH 059/172] instrumentation(flask): vanilla - Adapt vanilla/without_blinker to OTel spec Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/vanilla.py | 88 ++++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index 9775f1db..c763f5c9 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -4,15 +4,16 @@ import re import flask - -import opentracing -import opentracing.ext.tags as ext import wrapt +from opentelemetry.semconv.trace import SpanAttributes as ext +from opentelemetry import context, trace + from ...log import logger from ...singletons import agent, tracer from ...util.secrets import strip_secrets_from_query from .common import extract_custom_headers +from instana.propagators.format import Format path_tpl_re = re.compile('<.*>') @@ -20,28 +21,37 @@ def before_request_with_instana(*argv, **kwargs): try: env = flask.request.environ - ctx = tracer.extract(opentracing.Format.HTTP_HEADERS, env) + ctx = tracer.extract(Format.HTTP_HEADERS, env) + + span = tracer.start_span("wsgi", context=ctx) + flask.g.span = span - flask.g.scope = tracer.start_active_span('wsgi', child_of=ctx) - span = flask.g.scope.span + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + flask.g.token = token extract_custom_headers(span, env, format=True) - span.set_tag(ext.HTTP_METHOD, flask.request.method) - if 'PATH_INFO' in env: - span.set_tag(ext.HTTP_URL, env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) - span.set_tag("http.params", scrubbed_params) - if 'HTTP_HOST' in env: - span.set_tag("http.host", env['HTTP_HOST']) - - if hasattr(flask.request.url_rule, 'rule') and \ - path_tpl_re.search(flask.request.url_rule.rule) is not None: + span.set_attribute(ext.HTTP_METHOD, flask.request.method) + if "PATH_INFO" in env: + span.set_attribute(ext.HTTP_URL, env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + if "HTTP_HOST" in env: + span.set_attribute("http.host", env["HTTP_HOST"]) + + if ( + hasattr(flask.request.url_rule, "rule") + and path_tpl_re.search(flask.request.url_rule.rule) is not None + ): path_tpl = flask.request.url_rule.rule.replace("<", "{") path_tpl = path_tpl.replace(">", "}") - span.set_tag("http.path_tpl", path_tpl) + span.set_attribute("http.path_tpl", path_tpl) except: logger.debug("Flask before_request", exc_info=True) @@ -52,27 +62,28 @@ def after_request_with_instana(response): scope = None try: # If we're not tracing, just return - if not hasattr(flask.g, 'scope'): + if not hasattr(flask.g, "span"): return response - scope = flask.g.scope - if scope is not None: - span = scope.span + span = flask.g.span + if span is not None: if 500 <= response.status_code: span.mark_as_errored() - span.set_tag(ext.HTTP_STATUS_CODE, int(response.status_code)) + span.set_attribute(ext.HTTP_STATUS_CODE, int(response.status_code)) extract_custom_headers(span, response.headers, format=False) - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - response.headers.add('Server-Timing', "intid;desc=%s" % scope.span.context.trace_id) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers.add( + "Server-Timing", "intid;desc=%s" % span.context.trace_id + ) except: logger.debug("Flask after_request", exc_info=True) finally: - if scope is not None: - scope.close() - flask.g.scope = None + if span and span.is_recording(): + span.end() + flask.g.span = None return response @@ -81,14 +92,19 @@ def teardown_request_with_instana(*argv, **kwargs): In the case of exceptions, after_request_with_instana isn't called so we capture those cases here. """ - if hasattr(flask.g, 'scope') and flask.g.scope is not None: + if hasattr(flask.g, "span") and flask.g.span is not None: if len(argv) > 0 and argv[0] is not None: - scope = flask.g.scope - scope.span.log_exception(argv[0]) - if ext.HTTP_STATUS_CODE not in scope.span.tags: - scope.span.set_tag(ext.HTTP_STATUS_CODE, 500) - flask.g.scope.close() - flask.g.scope = None + span = flask.g.span + span.record_exception(argv[0]) + if ext.HTTP_STATUS_CODE not in span.attributes: + span.set_attribute(ext.HTTP_STATUS_CODE, 500) + if flask.g.span.is_recording(): + flask.g.span.end() + flask.g.span = None + + if hasattr(flask.g, "token") and flask.g.token is not None: + context.detach(flask.g.token) + flask.g.token = None @wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request') From fe8971b8ea8d84d28674aab09c45e62736f7409d Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 25 Jul 2024 16:18:10 +0530 Subject: [PATCH 060/172] fix: receive span_context as arg in start_span(), start_as_current_span() Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/common.py | 11 +++++------ src/instana/instrumentation/flask/with_blinker.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 373cbd2b..2da77bff 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -6,7 +6,6 @@ import flask from opentelemetry.semconv.trace import SpanAttributes as ext -from opentelemetry.trace import set_span_in_context from ...log import logger from ...singletons import tracer, agent @@ -19,9 +18,9 @@ def render_with_instana(wrapped, instance, argv, kwargs): return wrapped(*argv, **kwargs) parent_span = flask.g.span - parent_context = set_span_in_context(parent_span) + parent_context = parent_span.get_span_context() - with tracer.start_as_current_span("render", context=parent_context) as span: + with tracer.start_as_current_span("render", span_context=parent_context) as span: try: flask_version = tuple(map(int, flask.__version__.split('.'))) template = argv[1] if flask_version >= (2, 2, 0) else argv[0] @@ -59,7 +58,7 @@ def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): status_code = response.status_code if 500 <= status_code: - span.log_exception(exc) + span.record_exception(exc) span.set_attribute(ext.HTTP_STATUS_CODE, int(status_code)) @@ -70,8 +69,8 @@ def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): response.headers.add('Server-Timing', value) elif type(response.headers) is dict or hasattr(response.headers, "__dict__"): response.headers['Server-Timing'] = value - - span.end() + if span and span.is_recording(): + span.end() flask.g.span = None except: logger.debug("handle_user_exception_with_instana:", exc_info=True) diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index 22343344..850ba89d 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -23,9 +23,9 @@ def request_started_with_instana(sender, **extra): try: env = flask.request.environ - ctx = tracer.extract(Format.HTTP_HEADERS, env) + span_context = tracer.extract(Format.HTTP_HEADERS, env) - span = tracer.start_span("wsgi", context=ctx) + span = tracer.start_span("wsgi", span_context=span_context) flask.g.span = span ctx = trace.set_span_in_context(span) From aa0123b4d438754c67b3fbfee5224e1341531b2c Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 25 Jul 2024 16:39:05 +0530 Subject: [PATCH 061/172] fix(flask_app): Adapt to OTel spec Signed-off-by: Varsha GS --- tests/apps/flask_app/app.py | 44 ++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tests/apps/flask_app/app.py b/tests/apps/flask_app/app.py index d7042315..a0e9cc71 100755 --- a/tests/apps/flask_app/app.py +++ b/tests/apps/flask_app/app.py @@ -6,7 +6,9 @@ import os import logging -import opentracing.ext.tags as ext + +from opentelemetry.semconv.trace import SpanAttributes + from flask import jsonify, Response from wsgiref.simple_server import make_server from flask import Flask, redirect, render_template, render_template_string @@ -78,23 +80,29 @@ def username_hello(username): @app.route("/complex") -def gen_opentracing(): - with tracer.start_active_span('asteroid') as pscope: - pscope.span.set_tag(ext.COMPONENT, "Python simple example app") - pscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER) - pscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - pscope.span.set_tag(ext.HTTP_URL, "/python/simple/one") - pscope.span.set_tag(ext.HTTP_METHOD, "GET") - pscope.span.set_tag(ext.HTTP_STATUS_CODE, 200) - pscope.span.log_kv({"foo": "bar"}) - - with tracer.start_active_span('spacedust', child_of=pscope.span) as cscope: - cscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_CLIENT) - cscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - cscope.span.set_tag(ext.HTTP_URL, "/python/simple/two") - cscope.span.set_tag(ext.HTTP_METHOD, "POST") - cscope.span.set_tag(ext.HTTP_STATUS_CODE, 204) - cscope.span.set_baggage_item("someBaggage", "someValue") +def gen_opentelemetry(): + with tracer.start_as_current_span("asteroid") as pspan: + pspan.set_attribute(SpanAttributes.COMPONENT, "Python simple example app") + pspan.set_attribute( + SpanAttributes.SPAN_KIND, SpanAttributes.SPAN_KIND_RPC_SERVER + ) + pspan.set_attribute(SpanAttributes.PEER_HOSTNAME, "localhost") + pspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/one") + pspan.set_attribute(SpanAttributes.HTTP_METHOD, "GET") + pspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 200) + pspan.add_event(name="gen_opentelemetry", attributes={"foo": "bar"}) + + span_context = pspan.get_span_context() + + with tracer.start_active_span("spacedust", span_context=span_context) as cspan: + cspan.set_attribute( + SpanAttributes.SPAN_KIND, SpanAttributes.SPAN_KIND_RPC_CLIENT + ) + cspan.set_attribute(SpanAttributes.PEER_HOSTNAME, "localhost") + cspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/two") + cspan.set_attribute(SpanAttributes.HTTP_METHOD, "POST") + cspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 204) + cspan.set_baggage_item("someBaggage", "someValue") return "

🐍 Generated some OT spans... 🦄

" From de678771d25968e8fead369f87534288c057ed64 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 25 Jul 2024 16:41:46 +0530 Subject: [PATCH 062/172] tests(flask): Adapt unit tests to OTel spec - skip tests related to synthetic and suppression until they're handled properly Signed-off-by: Varsha GS --- tests/frameworks/test_flask.py | 141 +++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 61 deletions(-) diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 65bf0ea7..4df28533 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -6,16 +6,19 @@ import flask if hasattr(flask.signals, 'signals_available'): - from flask.signals import signals_available + from flask.signals import signals_available else: - # Beginning from 2.3.0 as stated in the notes - # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 - # "Signals are always available. blinker>=1.6.2 is a required dependency. - # The signals_available attribute is deprecated. #5056" - signals_available = True + # Beginning from 2.3.0 as stated in the notes + # https://flask.palletsprojects.com/en/2.3.x/changes/#version-2-3-0 + # "Signals are always available. blinker>=1.6.2 is a required dependency. + # The signals_available attribute is deprecated. #5056" + signals_available = True + +from opentelemetry.trace import SpanKind import tests.apps.flask_app from instana.singletons import tracer +from instana.span.span import get_current_span from ..helpers import testenv @@ -23,7 +26,7 @@ class TestFlask(unittest.TestCase): def setUp(self): """ Clear all spans before a test run """ self.http = urllib3.PoolManager() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() def tearDown(self): @@ -38,7 +41,7 @@ def test_vanilla_requests(self): self.assertEqual(1, len(spans)) def test_get_request(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() @@ -53,11 +56,11 @@ def test_get_request(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -66,7 +69,7 @@ def test_get_request(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -108,6 +111,7 @@ def test_get_request(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + @unittest.skip("Suppression is not yet handled") def test_get_request_with_suppression(self): headers = {'X-INSTANA-L':'0'} response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers) @@ -129,6 +133,7 @@ def test_get_request_with_suppression(self): # Assert that there are no spans in the recorded list self.assertEqual(spans, []) + @unittest.skip("Suppression is not yet handled") def test_get_request_with_suppression_and_w3c(self): headers = { 'X-INSTANA-L':'0', @@ -154,12 +159,13 @@ def test_get_request_with_suppression_and_w3c(self): # Assert that there are no spans in the recorded list self.assertEqual(spans, []) + @unittest.skip("Synthetic requests are not yet handled") def test_synthetic_request(self): headers = { 'X-INSTANA-SYNTHETIC': '1' } - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) spans = self.recorder.queued_spans() @@ -174,7 +180,7 @@ def test_synthetic_request(self): self.assertIsNone(test_span.sy) def test_render_template(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/render') spans = self.recorder.queued_spans() @@ -190,11 +196,11 @@ def test_render_template(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -203,7 +209,7 @@ def test_render_template(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, render_span.t) @@ -223,11 +229,11 @@ def test_render_template(self): # render self.assertEqual("render", render_span.n) - self.assertEqual(3, render_span.k) + self.assertEqual(SpanKind.INTERNAL, render_span.k) self.assertEqual('flask_render_template.html', render_span.data["render"]["name"]) self.assertEqual('template', render_span.data["render"]["type"]) - self.assertIsNone(render_span.data["log"]["message"]) - self.assertIsNone(render_span.data["log"]["parameters"]) + self.assertIsNone(render_span.data["event"]["message"]) + self.assertIsNone(render_span.data["event"]["parameters"]) # wsgi self.assertEqual("wsgi", wsgi_span.n) @@ -252,7 +258,7 @@ def test_render_template(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) def test_render_template_string(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/render_string') spans = self.recorder.queued_spans() @@ -268,11 +274,11 @@ def test_render_template_string(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -281,7 +287,7 @@ def test_render_template_string(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, render_span.t) @@ -301,11 +307,11 @@ def test_render_template_string(self): # render self.assertEqual("render", render_span.n) - self.assertEqual(3, render_span.k) + self.assertEqual(SpanKind.INTERNAL, render_span.k) self.assertEqual('(from string)', render_span.data["render"]["name"]) self.assertEqual('template', render_span.data["render"]["type"]) - self.assertIsNone(render_span.data["log"]["message"]) - self.assertIsNone(render_span.data["log"]["parameters"]) + self.assertIsNone(render_span.data["event"]["message"]) + self.assertIsNone(render_span.data["event"]["parameters"]) # wsgi self.assertEqual("wsgi", wsgi_span.n) @@ -330,7 +336,7 @@ def test_render_template_string(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) def test_301(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/301', redirect=False) spans = self.recorder.queued_spans() @@ -346,11 +352,11 @@ def test_301(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -359,7 +365,7 @@ def test_301(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -397,7 +403,7 @@ def test_301(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) def test_custom_404(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/custom-404') spans = self.recorder.queued_spans() @@ -426,7 +432,7 @@ def test_custom_404(self): # server_timing_value = "intid;desc=%s" % wsgi_span.t # self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -464,7 +470,7 @@ def test_custom_404(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) def test_404(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/11111111111') spans = self.recorder.queued_spans() @@ -493,7 +499,7 @@ def test_404(self): # server_timing_value = "intid;desc=%s" % wsgi_span.t # self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -531,7 +537,7 @@ def test_404(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) def test_500(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/500') spans = self.recorder.queued_spans() @@ -547,11 +553,11 @@ def test_500(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -560,7 +566,7 @@ def test_500(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -601,7 +607,7 @@ def test_render_error(self): if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/render_error') spans = self.recorder.queued_spans() @@ -631,7 +637,7 @@ def test_render_error(self): # server_timing_value = "intid;desc=%s" % wsgi_span.t # self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -648,8 +654,13 @@ def test_render_error(self): # error log self.assertEqual("log", log_span.n) - self.assertEqual('Exception on /render_error [GET]', log_span.data["log"]['message']) - self.assertEqual(" unexpected '}'", log_span.data["log"]['parameters']) + self.assertEqual( + "Exception on /render_error [GET]", log_span.data["event"]["message"] + ) + self.assertEqual( + " unexpected '}'", + log_span.data["event"]["parameters"], + ) # wsgi self.assertEqual("wsgi", wsgi_span.n) @@ -677,7 +688,7 @@ def test_exception(self): if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/exception') spans = self.recorder.queued_spans() @@ -692,7 +703,7 @@ def test_exception(self): self.assertTrue(response) self.assertEqual(500, response.status) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -711,9 +722,12 @@ def test_exception(self): # error log self.assertEqual("log", log_span.n) - self.assertEqual('Exception on /exception [GET]', log_span.data["log"]['message']) - self.assertEqual(" fake error", log_span.data["log"]['parameters']) - + self.assertEqual( + "Exception on /exception [GET]", log_span.data["event"]["message"] + ) + self.assertEqual( + " fake error", log_span.data["event"]["parameters"] + ) # wsgis self.assertEqual("wsgi", wsgi_span.n) @@ -738,7 +752,7 @@ def test_exception(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) def test_custom_exception_with_log(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/exception-invalid-usage') spans = self.recorder.queued_spans() @@ -755,11 +769,11 @@ def test_custom_exception_with_log(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -768,7 +782,7 @@ def test_custom_exception_with_log(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -786,8 +800,13 @@ def test_custom_exception_with_log(self): # error log self.assertEqual("log", log_span.n) - self.assertEqual('InvalidUsage error handler invoked', log_span.data["log"]['message']) - self.assertEqual(" ", log_span.data["log"]['parameters']) + self.assertEqual( + "InvalidUsage error handler invoked", log_span.data["event"]["message"] + ) + self.assertEqual( + " ", + log_span.data["event"]["parameters"], + ) # wsgi self.assertEqual("wsgi", wsgi_span.n) @@ -812,7 +831,7 @@ def test_custom_exception_with_log(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) def test_path_templates(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/users/Ricky/sayhello') spans = self.recorder.queued_spans() @@ -827,11 +846,11 @@ def test_path_templates(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -840,7 +859,7 @@ def test_path_templates(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) @@ -883,7 +902,7 @@ def test_response_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/response_headers') spans = self.recorder.queued_spans() @@ -897,11 +916,11 @@ def test_response_header_capture(self): self.assertEqual(200, response.status) self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -910,7 +929,7 @@ def test_response_header_capture(self): server_timing_value = "intid;desc=%s" % wsgi_span.t self.assertEqual(response.headers['Server-Timing'], server_timing_value) - self.assertIsNone(tracer.active_span) + self.assertFalse(get_current_span().is_recording()) # Same traceId self.assertEqual(test_span.t, urllib3_span.t) From d776f4efcaf66aaf658b862cf0284994981b0dd3 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 26 Jul 2024 15:10:51 +0530 Subject: [PATCH 063/172] fix(vanilla): receive span_context as arg in start_span() Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/vanilla.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index c763f5c9..a814a4a5 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -21,9 +21,9 @@ def before_request_with_instana(*argv, **kwargs): try: env = flask.request.environ - ctx = tracer.extract(Format.HTTP_HEADERS, env) + span_context = tracer.extract(Format.HTTP_HEADERS, env) - span = tracer.start_span("wsgi", context=ctx) + span = tracer.start_span("wsgi", span_context=span_context) flask.g.span = span ctx = trace.set_span_in_context(span) @@ -59,7 +59,7 @@ def before_request_with_instana(*argv, **kwargs): def after_request_with_instana(response): - scope = None + span = None try: # If we're not tracing, just return if not hasattr(flask.g, "span"): @@ -112,8 +112,8 @@ def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs): if not hasattr(instance, '_stan_wuz_here'): logger.debug("Flask(vanilla): Applying flask before/after instrumentation funcs") setattr(instance, "_stan_wuz_here", True) - instance.after_request(after_request_with_instana) instance.before_request(before_request_with_instana) + instance.after_request(after_request_with_instana) instance.teardown_request(teardown_request_with_instana) return wrapped(*argv, **kwargs) From 3ca2909611372f3b77b911bf251d8b5d074ca6d5 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 26 Jul 2024 15:12:42 +0530 Subject: [PATCH 064/172] style(with_blinker): Add type hints Signed-off-by: Varsha GS --- .../instrumentation/flask/with_blinker.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index 850ba89d..7cee28c6 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -4,6 +4,8 @@ import re import wrapt +from typing import Any, Tuple, Dict, Callable + from opentelemetry.semconv.trace import SpanAttributes as ext from opentelemetry import context, trace @@ -19,7 +21,7 @@ path_tpl_re = re.compile("<.*>") -def request_started_with_instana(sender, **extra): +def request_started_with_instana(sender: flask.app.Flask, **extra: Any) -> None: try: env = flask.request.environ @@ -58,7 +60,10 @@ def request_started_with_instana(sender, **extra): logger.debug("Flask request_started_with_instana", exc_info=True) -def request_finished_with_instana(sender, response, **extra): +def request_finished_with_instana( + sender: flask.app.Flask, response: flask.wrappers.Response, **extra: Any +) -> None: + span = None try: if not hasattr(flask.g, "span"): return @@ -83,7 +88,9 @@ def request_finished_with_instana(sender, response, **extra): span.end() -def log_exception_with_instana(sender, exception, **extra): +def log_exception_with_instana( + sender: flask.app.Flask, exception: Any, **extra: Any +) -> None: if hasattr(flask.g, "span") and flask.g.span is not None: span = flask.g.span if span is not None: @@ -98,7 +105,7 @@ def log_exception_with_instana(sender, exception, **extra): span.end() -def teardown_request_with_instana(*argv, **kwargs): +def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: """ In the case of exceptions, request_finished_with_instana isn't called so we capture those cases here. @@ -119,7 +126,12 @@ def teardown_request_with_instana(*argv, **kwargs): @wrapt.patch_function_wrapper("flask", "Flask.full_dispatch_request") -def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs): +def full_dispatch_request_with_instana( + wrapped: Callable[..., flask.wrappers.Response], + instance: flask.app.Flask, + argv: Tuple, + kwargs: Dict, +) -> flask.wrappers.Response: if not hasattr(instance, "_stan_wuz_here"): logger.debug( "Flask(blinker): Applying flask before/after instrumentation funcs" From 4f3cc847f4f015996cf87e0d21a5381baea5e69a Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 26 Jul 2024 15:17:48 +0530 Subject: [PATCH 065/172] tests(flask): Add tests to increase coverage Signed-off-by: Varsha GS --- tests/frameworks/test_flask.py | 129 ++++++++++++++++++++++++++++----- 1 file changed, 110 insertions(+), 19 deletions(-) diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 4df28533..2f6f9485 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -4,6 +4,7 @@ import unittest import urllib3 import flask +from unittest.mock import patch if hasattr(flask.signals, 'signals_available'): from flask.signals import signals_available @@ -23,26 +24,102 @@ class TestFlask(unittest.TestCase): - def setUp(self): + + def setUp(self) -> None: """ Clear all spans before a test run """ self.http = urllib3.PoolManager() self.recorder = tracer.span_processor self.recorder.clear_spans() - def tearDown(self): + def tearDown(self) -> None: """ Do nothing for now """ return None - def test_vanilla_requests(self): + def test_vanilla_requests(self) -> None: r = self.http.request('GET', testenv["wsgi_server"] + '/') self.assertEqual(r.status, 200) spans = self.recorder.queued_spans() self.assertEqual(1, len(spans)) - def test_get_request(self): + def test_get_request(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["wsgi_server"] + "/") + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + self.assertTrue(response) + self.assertEqual(200, response.status) + + self.assertIn("X-INSTANA-T", response.headers) + self.assertTrue(int(response.headers["X-INSTANA-T"], 16)) + self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + + self.assertIn("X-INSTANA-S", response.headers) + self.assertTrue(int(response.headers["X-INSTANA-S"], 16)) + self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + + self.assertIn("X-INSTANA-L", response.headers) + self.assertEqual(response.headers["X-INSTANA-L"], "1") + + self.assertIn("Server-Timing", response.headers) + server_timing_value = "intid;desc=%s" % wsgi_span.t + self.assertEqual(response.headers["Server-Timing"], server_timing_value) + + self.assertFalse(get_current_span().is_recording()) + + # Same traceId + self.assertEqual(test_span.t, urllib3_span.t) + self.assertEqual(urllib3_span.t, wsgi_span.t) + + # Parent relationships + 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) + self.assertIsNone(wsgi_span.ec) + + # wsgi + self.assertEqual("wsgi", wsgi_span.n) + self.assertEqual( + "127.0.0.1:" + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"] + ) + self.assertEqual("/", wsgi_span.data["http"]["url"]) + self.assertEqual("GET", wsgi_span.data["http"]["method"]) + self.assertEqual(200, wsgi_span.data["http"]["status"]) + self.assertIsNone(wsgi_span.data["http"]["error"]) + self.assertIsNone(wsgi_span.stack) + + # urllib3 + self.assertEqual("test", test_span.data["sdk"]["name"]) + self.assertEqual("urllib3", urllib3_span.n) + self.assertEqual(200, urllib3_span.data["http"]["status"]) + self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) + self.assertEqual("GET", urllib3_span.data["http"]["method"]) + self.assertIsNotNone(urllib3_span.stack) + self.assertTrue(type(urllib3_span.stack) is list) + self.assertTrue(len(urllib3_span.stack) > 1) + + # We should NOT have a path template for this route + self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + + def test_get_request_with_query_params(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/') + response = self.http.request( + "GET", testenv["wsgi_server"] + "/" + "?key1=val1&key2=val2" + ) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -93,6 +170,9 @@ def test_get_request(self): self.assertEqual("wsgi", wsgi_span.n) self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) self.assertEqual('/', wsgi_span.data["http"]["url"]) + self.assertEqual( + "key1=&key2=", wsgi_span.data["http"]["params"] + ) self.assertEqual('GET', wsgi_span.data["http"]["method"]) self.assertEqual(200, wsgi_span.data["http"]["status"]) self.assertIsNone(wsgi_span.data["http"]["error"]) @@ -112,7 +192,7 @@ def test_get_request(self): self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) @unittest.skip("Suppression is not yet handled") - def test_get_request_with_suppression(self): + def test_get_request_with_suppression(self) -> None: headers = {'X-INSTANA-L':'0'} response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers) @@ -134,7 +214,7 @@ def test_get_request_with_suppression(self): self.assertEqual(spans, []) @unittest.skip("Suppression is not yet handled") - def test_get_request_with_suppression_and_w3c(self): + def test_get_request_with_suppression_and_w3c(self) -> None: headers = { 'X-INSTANA-L':'0', 'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', @@ -160,7 +240,7 @@ def test_get_request_with_suppression_and_w3c(self): self.assertEqual(spans, []) @unittest.skip("Synthetic requests are not yet handled") - def test_synthetic_request(self): + def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' } @@ -179,7 +259,7 @@ def test_synthetic_request(self): self.assertIsNone(urllib3_span.sy) self.assertIsNone(test_span.sy) - def test_render_template(self): + def test_render_template(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/render') @@ -257,7 +337,7 @@ def test_render_template(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_render_template_string(self): + def test_render_template_string(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/render_string') @@ -335,7 +415,7 @@ def test_render_template_string(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_301(self): + def test_301(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/301', redirect=False) @@ -402,7 +482,7 @@ def test_301(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_custom_404(self): + def test_custom_404(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/custom-404') @@ -469,7 +549,7 @@ def test_custom_404(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_404(self): + def test_404(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/11111111111') @@ -536,7 +616,7 @@ def test_404(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_500(self): + def test_500(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/500') @@ -603,7 +683,7 @@ def test_500(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_render_error(self): + def test_render_error(self) -> None: if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") @@ -684,7 +764,7 @@ def test_render_error(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_exception(self): + def test_exception(self) -> None: if signals_available is True: raise unittest.SkipTest("Exceptions without handlers vary with blinker") @@ -751,7 +831,7 @@ def test_exception(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_custom_exception_with_log(self): + def test_custom_exception_with_log(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/exception-invalid-usage') @@ -830,7 +910,7 @@ def test_custom_exception_with_log(self): # We should NOT have a path template for this route self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) - def test_path_templates(self): + def test_path_templates(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/users/Ricky/sayhello') @@ -896,7 +976,7 @@ def test_path_templates(self): # We should have a reported path template for this route self.assertEqual("/users/{username}/sayhello", wsgi_span.data["http"]["path_tpl"]) - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: # Hack together a manual custom headers list from instana.singletons import agent original_extra_http_headers = agent.options.extra_http_headers @@ -974,3 +1054,14 @@ def test_response_header_capture(self): self.assertEqual("Ok too", wsgi_span.data["http"]["header"]["X-Capture-That"]) agent.options.extra_http_headers = original_extra_http_headers + + def test_request_started_exception(self) -> None: + with tracer.start_as_current_span("test"): + with patch( + "instana.singletons.tracer.extract", + side_effect=Exception("mocked error"), + ): + self.http.request("GET", testenv["wsgi_server"] + "/") + + spans = self.recorder.queued_spans() + assert len(spans) == 2 From 26e6b8cad704b0d6ee93d0c15d656af9b8dd51f6 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 26 Jul 2024 15:52:54 +0530 Subject: [PATCH 066/172] tests(flask): use py std assert statements Signed-off-by: Varsha GS --- tests/frameworks/test_flask.py | 1131 ++++++++++++++++---------------- 1 file changed, 582 insertions(+), 549 deletions(-) diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 2f6f9485..418222fb 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -37,83 +37,83 @@ def tearDown(self) -> None: def test_vanilla_requests(self) -> None: r = self.http.request('GET', testenv["wsgi_server"] + '/') - self.assertEqual(r.status, 200) + assert r.status == 200 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 def test_get_request(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request("GET", testenv["wsgi_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertIn("X-INSTANA-T", response.headers) - self.assertTrue(int(response.headers["X-INSTANA-T"], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn("X-INSTANA-S", response.headers) - self.assertTrue(int(response.headers["X-INSTANA-S"], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], "1") + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn("Server-Timing", response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers["Server-Timing"], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Synthetic - self.assertIsNone(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual( - "127.0.0.1:" + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"] + assert "wsgi" == wsgi_span.n + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] ) - self.assertEqual("/", wsgi_span.data["http"]["url"]) - self.assertEqual("GET", wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "/" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert testenv["wsgi_server"] + "/" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_get_request_with_query_params(self) -> None: with tracer.start_as_current_span("test"): @@ -122,74 +122,74 @@ def test_get_request_with_query_params(self) -> None: ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Synthetic - self.assertIsNone(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual( - "key1=&key2=", wsgi_span.data["http"]["params"] + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] ) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "/" == wsgi_span.data["http"]["url"] + assert wsgi_span.data["http"]["params"] == "key1=&key2=" + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert testenv["wsgi_server"] + "/" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None @unittest.skip("Suppression is not yet handled") def test_get_request_with_suppression(self) -> None: @@ -198,20 +198,20 @@ def test_get_request_with_suppression(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(response.headers.get('X-INSTANA-L', None), '0') + assert response.headers.get("X-INSTANA-L", None) == "0" # The traceparent has to be present - self.assertIsNotNone(response.headers.get('traceparent', None)) + assert response.headers.get("traceparent", None) is not None # The last digit of the traceparent has to be 0 - self.assertEqual(response.headers['traceparent'][-1], '0') + assert response.headers["traceparent"][-1] == "0" # This should not be present - self.assertIsNone(response.headers.get('tracestate', None)) + assert response.headers.get("tracestate", None) is None # Assert that there isn't any span, where level is not 0! - self.assertFalse(any(map(lambda x: x.l != 0, spans))) + assert any(map(lambda x: x.l != 0, spans)) is False # Assert that there are no spans in the recorded list - self.assertEqual(spans, []) + assert spans == [] @unittest.skip("Suppression is not yet handled") def test_get_request_with_suppression_and_w3c(self) -> None: @@ -224,20 +224,20 @@ def test_get_request_with_suppression_and_w3c(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(response.headers.get('X-INSTANA-L', None), '0') - self.assertIsNotNone(response.headers.get('traceparent', None)) - self.assertEqual(response.headers['traceparent'][-1], '0') + assert response.headers.get("X-INSTANA-L", None) == "0" + assert response.headers.get("traceparent", None) is not None + assert response.headers["traceparent"][-1] == "0" # The tracestate has to be present - self.assertIsNotNone(response.headers.get('tracestate', None)) + assert response.headers.get("tracestate", None) is not None # The 'in=' section can not be in the tracestate - self.assertTrue('in=' not in response.headers['tracestate']) + assert "in=" not in response.headers["tracestate"] # Assert that there isn't any span, where level is not 0! - self.assertFalse(any(map(lambda x: x.l != 0, spans))) + assert any(map(lambda x: x.l != 0, spans)) is False # Assert that there are no spans in the recorded list - self.assertEqual(spans, []) + assert spans == [] @unittest.skip("Synthetic requests are not yet handled") def test_synthetic_request(self) -> None: @@ -249,171 +249,178 @@ def test_synthetic_request(self) -> None: response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 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) + assert wsgi_span.sy + assert urllib3_span.sy is None + assert test_span.sy is None def test_render_template(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/render') spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 render_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, render_span.t) - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == render_span.t + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(render_span.p, wsgi_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + assert render_span.p == wsgi_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) - self.assertIsNone(render_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None + assert render_span.ec is None # render - self.assertEqual("render", render_span.n) - self.assertEqual(SpanKind.INTERNAL, render_span.k) - self.assertEqual('flask_render_template.html', render_span.data["render"]["name"]) - self.assertEqual('template', render_span.data["render"]["type"]) - self.assertIsNone(render_span.data["event"]["message"]) - self.assertIsNone(render_span.data["event"]["parameters"]) + assert "render" == render_span.n + assert SpanKind.INTERNAL == render_span.k + assert "flask_render_template.html" == render_span.data["render"]["name"] + assert "template" == render_span.data["render"]["type"] + assert render_span.data["event"]["message"] is None + assert render_span.data["event"]["parameters"] is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/render', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/render" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/render', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert testenv["wsgi_server"] + "/render" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_render_template_string(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/render_string') spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 render_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, render_span.t) - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == render_span.t + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(render_span.p, wsgi_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + assert render_span.p == wsgi_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) - self.assertIsNone(render_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None + assert render_span.ec is None # render - self.assertEqual("render", render_span.n) - self.assertEqual(SpanKind.INTERNAL, render_span.k) - self.assertEqual('(from string)', render_span.data["render"]["name"]) - self.assertEqual('template', render_span.data["render"]["type"]) - self.assertIsNone(render_span.data["event"]["message"]) - self.assertIsNone(render_span.data["event"]["parameters"]) + assert "render" == render_span.n + assert SpanKind.INTERNAL == render_span.k + assert "(from string)" == render_span.data["render"]["name"] + assert "template" == render_span.data["render"]["type"] + assert render_span.data["event"]["message"] is None + assert render_span.data["event"]["parameters"] is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/render_string', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/render_string" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/render_string', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert ( + testenv["wsgi_server"] + "/render_string" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_301(self) -> None: with tracer.start_as_current_span("test"): @@ -421,66 +428,68 @@ def test_301(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(301, response.status) + assert response + assert 301 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(None, urllib3_span.ec) - self.assertEqual(None, wsgi_span.ec) + assert test_span.ec is None + assert None == urllib3_span.ec + assert None == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/301', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(301, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/301" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 301 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(301, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/301', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 301 == urllib3_span.data["http"]["status"] + assert testenv["wsgi_server"] + "/301" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_custom_404(self) -> None: with tracer.start_as_current_span("test"): @@ -488,66 +497,70 @@ def test_custom_404(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status - # self.assertIn('X-INSTANA-T', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - # self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + # assert 'X-INSTANA-T' in response.headers + # assert int(response.headers['X-INSTANA-T']) == 16 + # assert response.headers['X-INSTANA-T'] == wsgi_span.t # - # self.assertIn('X-INSTANA-S', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - # self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + # assert 'X-INSTANA-S' in response.headers + # assert int(response.headers['X-INSTANA-S']) == 16 + # assert response.headers['X-INSTANA-S'] == wsgi_span.s # - # self.assertIn('X-INSTANA-L', response.headers) - # self.assertEqual(response.headers['X-INSTANA-L'], '1') + # assert 'X-INSTANA-L' in response.headers + # assert response.headers['X-INSTANA-L'] == '1' # - # self.assertIn('Server-Timing', response.headers) + # assert 'Server-Timing' in response.headers # server_timing_value = "intid;desc=%s" % wsgi_span.t - # self.assertEqual(response.headers['Server-Timing'], server_timing_value) + # assert response.headers['Server-Timing'] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(None, urllib3_span.ec) - self.assertEqual(None, wsgi_span.ec) + assert test_span.ec is None + assert None == urllib3_span.ec + assert None == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/custom-404', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/custom-404" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 404 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/custom-404', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 404 == urllib3_span.data["http"]["status"] + assert ( + testenv["wsgi_server"] + "/custom-404" == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_404(self) -> None: with tracer.start_as_current_span("test"): @@ -555,66 +568,70 @@ def test_404(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status - # self.assertIn('X-INSTANA-T', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - # self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + # assert 'X-INSTANA-T' in response.headers + # assert int(response.headers['X-INSTANA-T']) == 16 + # assert response.headers['X-INSTANA-T'] == wsgi_span.t # - # self.assertIn('X-INSTANA-S', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - # self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + # assert 'X-INSTANA-S' in response.headers + # assert int(response.headers['X-INSTANA-S']) == 16 + # assert response.headers['X-INSTANA-S'] == wsgi_span.s # - # self.assertIn('X-INSTANA-L', response.headers) - # self.assertEqual(response.headers['X-INSTANA-L'], '1') + # assert 'X-INSTANA-L' in response.headers + # assert response.headers['X-INSTANA-L'] == '1' # - # self.assertIn('Server-Timing', response.headers) + # assert 'Server-Timing' in response.headers # server_timing_value = "intid;desc=%s" % wsgi_span.t - # self.assertEqual(response.headers['Server-Timing'], server_timing_value) + # assert response.headers['Server-Timing'] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(None, urllib3_span.ec) - self.assertEqual(None, wsgi_span.ec) + assert test_span.ec is None + assert None == urllib3_span.ec + assert None == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/11111111111', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/11111111111" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 404 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/11111111111', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 404 == urllib3_span.data["http"]["status"] + assert ( + testenv["wsgi_server"] + "/11111111111" == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_500(self) -> None: with tracer.start_as_current_span("test"): @@ -622,66 +639,68 @@ def test_500(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/500', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/500" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 500 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/500', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 500 == urllib3_span.data["http"]["status"] + assert testenv["wsgi_server"] + "/500" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_render_error(self) -> None: if signals_available is True: @@ -692,77 +711,79 @@ def test_render_error(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 log_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status - # self.assertIn('X-INSTANA-T', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - # self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + # assert 'X-INSTANA-T' in response.headers + # assert int(response.headers['X-INSTANA-T']) == 16 + # assert response.headers['X-INSTANA-T'] == wsgi_span.t # - # self.assertIn('X-INSTANA-S', response.headers) - # self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - # self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + # assert 'X-INSTANA-S' in response.headers + # assert int(response.headers['X-INSTANA-S']) == 16 + # assert response.headers['X-INSTANA-S'] == wsgi_span.s # - # self.assertIn('X-INSTANA-L', response.headers) - # self.assertEqual(response.headers['X-INSTANA-L'], '1') + # assert 'X-INSTANA-L' in response.headers + # assert response.headers['X-INSTANA-L'] == '1' # - # self.assertIn('Server-Timing', response.headers) + # assert 'Server-Timing' in response.headers # server_timing_value = "intid;desc=%s" % wsgi_span.t - # self.assertEqual(response.headers['Server-Timing'], server_timing_value) + # assert response.headers['Server-Timing'] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec # error log - self.assertEqual("log", log_span.n) - self.assertEqual( - "Exception on /render_error [GET]", log_span.data["event"]["message"] - ) - self.assertEqual( - " unexpected '}'", - log_span.data["event"]["parameters"], + assert "log" == log_span.n + assert log_span.data["event"]["message"] == "Exception on /render_error [GET]" + assert ( + log_span.data["event"]["parameters"] + == " unexpected '}'" ) # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/render_error', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/render_error" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 500 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/render_error', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 500 == urllib3_span.data["http"]["status"] + assert ( + testenv["wsgi_server"] + "/render_error" == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_exception(self) -> None: if signals_available is True: @@ -773,63 +794,61 @@ def test_exception(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 log_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(log_span.p, wsgi_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + assert log_span.p == wsgi_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) - self.assertEqual(1, log_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec + assert 1 == log_span.ec # error log - self.assertEqual("log", log_span.n) - self.assertEqual( - "Exception on /exception [GET]", log_span.data["event"]["message"] - ) - self.assertEqual( - " fake error", log_span.data["event"]["parameters"] - ) + assert "log" == log_span.n + assert log_span.data["event"]["message"] == "Exception on /exception [GET]" + assert log_span.data["event"]["parameters"] == " fake error" - # wsgis - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/exception', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + # wsgi + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/exception" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 500 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/exception', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 500 == urllib3_span.data["http"]["status"] + assert testenv["wsgi_server"] + "/exception" == urllib3_span.data["http"]["url"] + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_custom_exception_with_log(self) -> None: with tracer.start_as_current_span("test"): @@ -837,144 +856,152 @@ def test_custom_exception_with_log(self) -> None: spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 log_span = spans[0] wsgi_span = spans[1] urllib3_span = spans[2] test_span = spans[3] - self.assertTrue(response) - self.assertEqual(502, response.status) + assert response + assert 502 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) - self.assertEqual(1, log_span.ec) + assert test_span.ec is None + assert 1 == urllib3_span.ec + assert 1 == wsgi_span.ec + assert 1 == log_span.ec # error log - self.assertEqual("log", log_span.n) - self.assertEqual( - "InvalidUsage error handler invoked", log_span.data["event"]["message"] - ) - self.assertEqual( - " ", - log_span.data["event"]["parameters"], + assert "log" == log_span.n + assert log_span.data["event"]["message"] == "InvalidUsage error handler invoked" + assert ( + log_span.data["event"]["parameters"] + == " " ) # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/exception-invalid-usage', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(502, wsgi_span.data["http"]["status"]) - self.assertEqual('Simulated custom exception', wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/exception-invalid-usage" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 502 == wsgi_span.data["http"]["status"] + assert "Simulated custom exception" == wsgi_span.data["http"]["error"] + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(502, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/exception-invalid-usage', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 502 == urllib3_span.data["http"]["status"] + assert ( + testenv["wsgi_server"] + "/exception-invalid-usage" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should NOT have a path template for this route - self.assertIsNone(wsgi_span.data["http"]["path_tpl"]) + assert wsgi_span.data["http"]["path_tpl"] is None def test_path_templates(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/users/Ricky/sayhello') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/users/Ricky/sayhello', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/users/Ricky/sayhello" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + '/users/Ricky/sayhello', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert ( + testenv["wsgi_server"] + "/users/Ricky/sayhello" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # We should have a reported path template for this route - self.assertEqual("/users/{username}/sayhello", wsgi_span.data["http"]["path_tpl"]) + assert "/users/{username}/sayhello" == wsgi_span.data["http"]["path_tpl"] def test_response_header_capture(self) -> None: # Hack together a manual custom headers list @@ -986,72 +1013,78 @@ def test_response_header_capture(self) -> None: response = self.http.request('GET', testenv["wsgi_server"] + '/response_headers') spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers["X-INSTANA-T"], str(wsgi_span.t)) + assert response + assert response.status == 200 + assert "X-INSTANA-T" in response.headers + assert (int(response.headers["X-INSTANA-T"]), 16) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers["X-INSTANA-S"], str(wsgi_span.s)) + assert "X-INSTANA-S" in response.headers + assert (int(response.headers["X-INSTANA-S"]), 16) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertFalse(get_current_span().is_recording()) + assert get_current_span().is_recording() is False # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Synthetic - self.assertIsNone(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/response_headers", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert 200 == urllib3_span.data["http"]["status"] + assert ( + testenv["wsgi_server"] + "/response_headers" + == urllib3_span.data["http"]["url"] + ) + assert "GET" == urllib3_span.data["http"]["method"] + assert urllib3_span.stack is not None + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/response_headers', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) - - self.assertIn("X-Capture-This", wsgi_span.data["http"]["header"]) - self.assertEqual("Ok", wsgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", wsgi_span.data["http"]["header"]) - self.assertEqual("Ok too", wsgi_span.data["http"]["header"]["X-Capture-That"]) + assert "wsgi" == wsgi_span.n + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/response_headers" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None + + assert "X-Capture-This" in wsgi_span.data["http"]["header"] + assert "Ok" == wsgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in wsgi_span.data["http"]["header"] + + assert "Ok too" == wsgi_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers From fc5713d990d6c63b0e67f5b1793475c001b4a5c9 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 26 Jul 2024 21:32:33 +0530 Subject: [PATCH 067/172] style: fix import statements Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/__init__.py | 2 +- src/instana/instrumentation/flask/common.py | 4 ++-- src/instana/instrumentation/flask/vanilla.py | 8 ++++---- src/instana/instrumentation/flask/with_blinker.py | 8 ++++---- tests/frameworks/test_flask.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/instana/instrumentation/flask/__init__.py b/src/instana/instrumentation/flask/__init__.py index 07cbbd39..7d85abcd 100644 --- a/src/instana/instrumentation/flask/__init__.py +++ b/src/instana/instrumentation/flask/__init__.py @@ -19,7 +19,7 @@ # The signals_available attribute is deprecated. #5056" signals_available = True - from . import common + from instana.instrumentation.flask import common if signals_available is True: import instana.instrumentation.flask.with_blinker diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 2da77bff..80fa1ed8 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -7,8 +7,8 @@ from opentelemetry.semconv.trace import SpanAttributes as ext -from ...log import logger -from ...singletons import tracer, agent +from instana.log import logger +from instana.singletons import tracer, agent from instana.propagators.format import Format @wrapt.patch_function_wrapper('flask', 'templating._render') diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index a814a4a5..b475d0fb 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -9,10 +9,10 @@ from opentelemetry.semconv.trace import SpanAttributes as ext from opentelemetry import context, trace -from ...log import logger -from ...singletons import agent, tracer -from ...util.secrets import strip_secrets_from_query -from .common import extract_custom_headers +from instana.log import logger +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query +from instana.instrumentation.flask.common import extract_custom_headers from instana.propagators.format import Format path_tpl_re = re.compile('<.*>') diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index 7cee28c6..400c1d04 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -9,10 +9,10 @@ from opentelemetry.semconv.trace import SpanAttributes as ext from opentelemetry import context, trace -from ...log import logger -from ...util.secrets import strip_secrets_from_query -from ...singletons import agent, tracer -from .common import extract_custom_headers +from instana.log import logger +from instana.util.secrets import strip_secrets_from_query +from instana.singletons import agent, tracer +from instana.instrumentation.flask.common import extract_custom_headers from instana.propagators.format import Format import flask diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 418222fb..7be920a8 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -20,7 +20,7 @@ import tests.apps.flask_app from instana.singletons import tracer from instana.span.span import get_current_span -from ..helpers import testenv +from tests.helpers import testenv class TestFlask(unittest.TestCase): From 75a76caba8ab119b47f755df3a7c2fbeaf90e7da Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 26 Jul 2024 21:35:32 +0530 Subject: [PATCH 068/172] fix(tests): add tests to handle `got_request_exception` signal Signed-off-by: Varsha GS --- tests/apps/flask_app/app.py | 5 +++ tests/frameworks/test_flask.py | 65 ++++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/tests/apps/flask_app/app.py b/tests/apps/flask_app/app.py index a0e9cc71..104fbe59 100755 --- a/tests/apps/flask_app/app.py +++ b/tests/apps/flask_app/app.py @@ -147,6 +147,11 @@ def exception(): raise Exception('fake error') +@app.route("/got_request_exception") +def got_request_exception(): + raise RuntimeError() + + @app.route("/exception-invalid-usage") def exception_invalid_usage(): raise InvalidUsage("Simulated custom exception", status_code=502) diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 7be920a8..5ee11a7c 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -57,11 +57,11 @@ def test_get_request(self) -> None: assert response.status == 200 assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -132,11 +132,11 @@ def test_get_request_with_query_params(self) -> None: assert response.status == 200 assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -275,11 +275,11 @@ def test_render_template(self) -> None: assert response.status == 200 assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -355,11 +355,11 @@ def test_render_template_string(self) -> None: assert response.status == 200 assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -438,11 +438,11 @@ def test_301(self) -> None: assert 301 == response.status assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -649,11 +649,11 @@ def test_500(self) -> None: assert 500 == response.status assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -867,11 +867,11 @@ def test_custom_exception_with_log(self) -> None: assert 502 == response.status assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -947,11 +947,11 @@ def test_path_templates(self) -> None: assert response.status == 200 assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -1022,11 +1022,11 @@ def test_response_header_capture(self) -> None: assert response assert response.status == 200 assert "X-INSTANA-T" in response.headers - assert (int(response.headers["X-INSTANA-T"]), 16) + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) assert "X-INSTANA-S" in response.headers - assert (int(response.headers["X-INSTANA-S"]), 16) + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) assert "X-INSTANA-L" in response.headers @@ -1098,3 +1098,32 @@ def test_request_started_exception(self) -> None: spans = self.recorder.queued_spans() assert len(spans) == 2 + + def test_got_request_exception(self) -> None: + response = self.http.request( + "GET", testenv["wsgi_server"] + "/got_request_exception" + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + wsgi_span = spans[0] + + assert response + assert 500 == response.status + + assert get_current_span().is_recording() is False + + # Error logging + assert wsgi_span.ec == 1 + + # wsgi + assert wsgi_span.n == "wsgi" + assert ( + "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + ) + assert "/got_request_exception" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert wsgi_span.data["http"]["status"] == 500 + assert wsgi_span.data["http"]["error"] == "RuntimeError()" + assert wsgi_span.stack is None From 7a520d94ca59e1c01c5ee0215874a0e5f7ef63d9 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 26 Jul 2024 21:37:17 +0530 Subject: [PATCH 069/172] fix: fix deprecation warning - DeprecationWarning: The '__version__' attribute is deprecated and will be removed in Flask 3.1. Use feature detection or 'importlib.metadata.version("flask")' instead. flask_version = tuple(map(int, flask.__version__.split('.'))) Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 80fa1ed8..c36735a7 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -4,6 +4,7 @@ import wrapt import flask +from importlib.metadata import version from opentelemetry.semconv.trace import SpanAttributes as ext @@ -22,7 +23,7 @@ def render_with_instana(wrapped, instance, argv, kwargs): with tracer.start_as_current_span("render", span_context=parent_context) as span: try: - flask_version = tuple(map(int, flask.__version__.split('.'))) + flask_version = tuple(map(int, version("flask").split("."))) template = argv[1] if flask_version >= (2, 2, 0) else argv[0] span.set_attribute("type", "template") From 93d6baaffe91eddb552459e44fb9d187ac32fa07 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 29 Jul 2024 15:27:30 +0530 Subject: [PATCH 070/172] style: Add typehints Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/common.py | 29 ++++++++++++++++++-- src/instana/instrumentation/flask/vanilla.py | 16 ++++++++--- tests/frameworks/test_flask.py | 4 +++ 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index c36735a7..7d1afefe 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -5,15 +5,31 @@ import wrapt import flask from importlib.metadata import version +from typing import Callable, Tuple, Dict, Any, TYPE_CHECKING, Union from opentelemetry.semconv.trace import SpanAttributes as ext from instana.log import logger from instana.singletons import tracer, agent from instana.propagators.format import Format +from instana.instrumentation.flask import signals_available + + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from werkzeug.exceptions import HTTPException + from flask.typing import ResponseReturnValue + + if signals_available: + from werkzeug.datastructures.headers import Headers + else: + from werkzeug.datastructures import Headers + @wrapt.patch_function_wrapper('flask', 'templating._render') -def render_with_instana(wrapped, instance, argv, kwargs): +def render_with_instana( + wrapped: Callable[..., str], instance: Any, argv: Tuple, kwargs: Dict +) -> str: # If we're not tracing, just return if not (hasattr(flask, "g") and hasattr(flask.g, "span")): return wrapped(*argv, **kwargs) @@ -39,7 +55,12 @@ def render_with_instana(wrapped, instance, argv, kwargs): @wrapt.patch_function_wrapper('flask', 'Flask.handle_user_exception') -def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): +def handle_user_exception_with_instana( + wrapped: Callable[..., Union["HTTPException", "ResponseReturnValue"]], + instance: flask.app.Flask, + argv: Tuple, + kwargs: Dict, +) -> Union["HTTPException", "ResponseReturnValue"]: # Call original and then try to do post processing response = wrapped(*argv, **kwargs) @@ -79,7 +100,9 @@ def handle_user_exception_with_instana(wrapped, instance, argv, kwargs): return response -def extract_custom_headers(span, headers, format): +def extract_custom_headers( + span: "InstanaSpan", headers: Union[dict[str, Any], "Headers"], format: bool +) -> None: if agent.options.extra_http_headers is None: return try: diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index b475d0fb..f6c7f51f 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -5,6 +5,7 @@ import re import flask import wrapt +from typing import Any, Callable, Tuple, Dict from opentelemetry.semconv.trace import SpanAttributes as ext from opentelemetry import context, trace @@ -18,7 +19,7 @@ path_tpl_re = re.compile('<.*>') -def before_request_with_instana(*argv, **kwargs): +def before_request_with_instana(*argv: Any, **kwargs: Any) -> None: try: env = flask.request.environ span_context = tracer.extract(Format.HTTP_HEADERS, env) @@ -58,7 +59,9 @@ def before_request_with_instana(*argv, **kwargs): return None -def after_request_with_instana(response): +def after_request_with_instana( + response: flask.wrappers.Response, +) -> flask.wrappers.Response: span = None try: # If we're not tracing, just return @@ -87,7 +90,7 @@ def after_request_with_instana(response): return response -def teardown_request_with_instana(*argv, **kwargs): +def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: """ In the case of exceptions, after_request_with_instana isn't called so we capture those cases here. @@ -108,7 +111,12 @@ def teardown_request_with_instana(*argv, **kwargs): @wrapt.patch_function_wrapper('flask', 'Flask.full_dispatch_request') -def full_dispatch_request_with_instana(wrapped, instance, argv, kwargs): +def full_dispatch_request_with_instana( + wrapped: Callable[..., flask.wrappers.Response], + instance: flask.app.Flask, + argv: Tuple, + kwargs: Dict, +) -> flask.wrappers.Response: if not hasattr(instance, '_stan_wuz_here'): logger.debug("Flask(vanilla): Applying flask before/after instrumentation funcs") setattr(instance, "_stan_wuz_here", True) diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 5ee11a7c..d2234831 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -1099,6 +1099,10 @@ def test_request_started_exception(self) -> None: spans = self.recorder.queued_spans() assert len(spans) == 2 + @unittest.skipIf( + not signals_available, + "log_exception_with_instana needs to be covered only with blinker", + ) def test_got_request_exception(self) -> None: response = self.http.request( "GET", testenv["wsgi_server"] + "/got_request_exception" From 1b20e47545fc9fd49230551e371ce3e4eb3f3631 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 29 Jul 2024 20:57:55 +0530 Subject: [PATCH 071/172] fix(tests): fetch log msg from data["log"] Signed-off-by: Varsha GS --- tests/frameworks/test_flask.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index d2234831..22627f47 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -312,8 +312,8 @@ def test_render_template(self) -> None: assert SpanKind.INTERNAL == render_span.k assert "flask_render_template.html" == render_span.data["render"]["name"] assert "template" == render_span.data["render"]["type"] - assert render_span.data["event"]["message"] is None - assert render_span.data["event"]["parameters"] is None + assert render_span.data["log"]["message"] is None + assert render_span.data["log"]["parameters"] is None # wsgi assert "wsgi" == wsgi_span.n @@ -392,8 +392,8 @@ def test_render_template_string(self) -> None: assert SpanKind.INTERNAL == render_span.k assert "(from string)" == render_span.data["render"]["name"] assert "template" == render_span.data["render"]["type"] - assert render_span.data["event"]["message"] is None - assert render_span.data["event"]["parameters"] is None + assert render_span.data["log"]["message"] is None + assert render_span.data["log"]["parameters"] is None # wsgi assert "wsgi" == wsgi_span.n @@ -753,9 +753,9 @@ def test_render_error(self) -> None: # error log assert "log" == log_span.n - assert log_span.data["event"]["message"] == "Exception on /render_error [GET]" + assert log_span.data["log"]["message"] == "Exception on /render_error [GET]" assert ( - log_span.data["event"]["parameters"] + log_span.data["log"]["parameters"] == " unexpected '}'" ) @@ -823,8 +823,8 @@ def test_exception(self) -> None: # error log assert "log" == log_span.n - assert log_span.data["event"]["message"] == "Exception on /exception [GET]" - assert log_span.data["event"]["parameters"] == " fake error" + assert log_span.data["log"]["message"] == "Exception on /exception [GET]" + assert log_span.data["log"]["parameters"] == " fake error" # wsgi assert "wsgi" == wsgi_span.n @@ -899,9 +899,9 @@ def test_custom_exception_with_log(self) -> None: # error log assert "log" == log_span.n - assert log_span.data["event"]["message"] == "InvalidUsage error handler invoked" + assert log_span.data["log"]["message"] == "InvalidUsage error handler invoked" assert ( - log_span.data["event"]["parameters"] + log_span.data["log"]["parameters"] == " " ) From 1be912a96f002f51c1c578dbe412ac907a5ad014 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 31 Jul 2024 11:27:35 +0530 Subject: [PATCH 072/172] fix: Add sem-conv to requirements file Signed-off-by: Varsha GS --- pyproject.toml | 1 + src/instana/instrumentation/flask/common.py | 4 ++-- src/instana/instrumentation/flask/vanilla.py | 14 ++++++++------ .../instrumentation/flask/with_blinker.py | 16 +++++++++------- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 59617c46..a72eab13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dependencies = [ "six>=1.12.0", "urllib3>=1.26.5", "opentelemetry-api>=1.26.0", + "opentelemetry-semantic-conventions>=0.47b0", ] [project.entry-points."instana"] diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 7d1afefe..442aaad0 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -7,7 +7,7 @@ from importlib.metadata import version from typing import Callable, Tuple, Dict, Any, TYPE_CHECKING, Union -from opentelemetry.semconv.trace import SpanAttributes as ext +from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger from instana.singletons import tracer, agent @@ -82,7 +82,7 @@ def handle_user_exception_with_instana( if 500 <= status_code: span.record_exception(exc) - span.set_attribute(ext.HTTP_STATUS_CODE, int(status_code)) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, int(status_code)) if hasattr(response, 'headers'): tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index f6c7f51f..9a52b3ea 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -7,7 +7,7 @@ import wrapt from typing import Any, Callable, Tuple, Dict -from opentelemetry.semconv.trace import SpanAttributes as ext +from opentelemetry.semconv.trace import SpanAttributes from opentelemetry import context, trace from instana.log import logger @@ -33,9 +33,9 @@ def before_request_with_instana(*argv: Any, **kwargs: Any) -> None: extract_custom_headers(span, env, format=True) - span.set_attribute(ext.HTTP_METHOD, flask.request.method) + span.set_attribute(SpanAttributes.HTTP_METHOD, flask.request.method) if "PATH_INFO" in env: - span.set_attribute(ext.HTTP_URL, env["PATH_INFO"]) + span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) if "QUERY_STRING" in env and len(env["QUERY_STRING"]): scrubbed_params = strip_secrets_from_query( env["QUERY_STRING"], @@ -74,7 +74,9 @@ def after_request_with_instana( if 500 <= response.status_code: span.mark_as_errored() - span.set_attribute(ext.HTTP_STATUS_CODE, int(response.status_code)) + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, int(response.status_code) + ) extract_custom_headers(span, response.headers, format=False) tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) @@ -99,8 +101,8 @@ def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: if len(argv) > 0 and argv[0] is not None: span = flask.g.span span.record_exception(argv[0]) - if ext.HTTP_STATUS_CODE not in span.attributes: - span.set_attribute(ext.HTTP_STATUS_CODE, 500) + if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) if flask.g.span.is_recording(): flask.g.span.end() flask.g.span = None diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index 400c1d04..70823908 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -6,7 +6,7 @@ import wrapt from typing import Any, Tuple, Dict, Callable -from opentelemetry.semconv.trace import SpanAttributes as ext +from opentelemetry.semconv.trace import SpanAttributes from opentelemetry import context, trace from instana.log import logger @@ -36,9 +36,9 @@ def request_started_with_instana(sender: flask.app.Flask, **extra: Any) -> None: extract_custom_headers(span, env, format=True) - span.set_attribute(ext.HTTP_METHOD, flask.request.method) + span.set_attribute(SpanAttributes.HTTP_METHOD, flask.request.method) if "PATH_INFO" in env: - span.set_attribute(ext.HTTP_URL, env["PATH_INFO"]) + span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) if "QUERY_STRING" in env and len(env["QUERY_STRING"]): scrubbed_params = strip_secrets_from_query( env["QUERY_STRING"], @@ -74,7 +74,9 @@ def request_finished_with_instana( if 500 <= response.status_code: span.mark_as_errored() - span.set_attribute(ext.HTTP_STATUS_CODE, int(response.status_code)) + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, int(response.status_code) + ) extract_custom_headers(span, response.headers, format=False) tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) @@ -100,7 +102,7 @@ def log_exception_with_instana( # d0bf462866289ad8bfe29b6e4e1e0f531003ab34/src/flask/app.py#L1379 # The `got_request_exception` signal, is only sent by # the `handle_exception` method which "always causes a 500" - span.set_attribute(ext.HTTP_STATUS_CODE, 500) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) if span.is_recording(): span.end() @@ -114,8 +116,8 @@ def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: if len(argv) > 0 and argv[0] is not None: span = flask.g.span span.record_exception(argv[0]) - if ext.HTTP_STATUS_CODE not in span.attributes: - span.set_attribute(ext.HTTP_STATUS_CODE, 500) + if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) if flask.g.span.is_recording(): flask.g.span.end() flask.g.span = None From c07fd979940b31ca5b3b310705043698c859b162 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 31 Jul 2024 11:45:12 +0530 Subject: [PATCH 073/172] fix: modify stmts with `is not None` Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/common.py | 4 ++-- src/instana/instrumentation/flask/vanilla.py | 13 ++++++------- .../instrumentation/flask/with_blinker.py | 18 ++++++++---------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 442aaad0..3a72ed4a 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -67,10 +67,10 @@ def handle_user_exception_with_instana( try: exc = argv[0] - if hasattr(flask.g, "span") and flask.g.span is not None: + if hasattr(flask.g, "span") and flask.g.span: span = flask.g.span - if response is not None: + if response: if isinstance(response, tuple): status_code = response[1] else: diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index 9a52b3ea..b750ed68 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -46,9 +46,8 @@ def before_request_with_instana(*argv: Any, **kwargs: Any) -> None: if "HTTP_HOST" in env: span.set_attribute("http.host", env["HTTP_HOST"]) - if ( - hasattr(flask.request.url_rule, "rule") - and path_tpl_re.search(flask.request.url_rule.rule) is not None + if hasattr(flask.request.url_rule, "rule") and path_tpl_re.search( + flask.request.url_rule.rule ): path_tpl = flask.request.url_rule.rule.replace("<", "{") path_tpl = path_tpl.replace(">", "}") @@ -69,7 +68,7 @@ def after_request_with_instana( return response span = flask.g.span - if span is not None: + if span: if 500 <= response.status_code: span.mark_as_errored() @@ -97,8 +96,8 @@ def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: In the case of exceptions, after_request_with_instana isn't called so we capture those cases here. """ - if hasattr(flask.g, "span") and flask.g.span is not None: - if len(argv) > 0 and argv[0] is not None: + if hasattr(flask.g, "span") and flask.g.span: + if len(argv) > 0 and argv[0]: span = flask.g.span span.record_exception(argv[0]) if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: @@ -107,7 +106,7 @@ def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: flask.g.span.end() flask.g.span = None - if hasattr(flask.g, "token") and flask.g.token is not None: + if hasattr(flask.g, "token") and flask.g.token: context.detach(flask.g.token) flask.g.token = None diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index 70823908..cc57f877 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -49,9 +49,8 @@ def request_started_with_instana(sender: flask.app.Flask, **extra: Any) -> None: if "HTTP_HOST" in env: span.set_attribute("http.host", env["HTTP_HOST"]) - if ( - hasattr(flask.request.url_rule, "rule") - and path_tpl_re.search(flask.request.url_rule.rule) is not None + if hasattr(flask.request.url_rule, "rule") and path_tpl_re.search( + flask.request.url_rule.rule ): path_tpl = flask.request.url_rule.rule.replace("<", "{") path_tpl = path_tpl.replace(">", "}") @@ -69,8 +68,7 @@ def request_finished_with_instana( return span = flask.g.span - if span is not None: - + if span: if 500 <= response.status_code: span.mark_as_errored() @@ -93,9 +91,9 @@ def request_finished_with_instana( def log_exception_with_instana( sender: flask.app.Flask, exception: Any, **extra: Any ) -> None: - if hasattr(flask.g, "span") and flask.g.span is not None: + if hasattr(flask.g, "span") and flask.g.span: span = flask.g.span - if span is not None: + if span: span.record_exception(exception) # As of Flask 2.3.x: # https://github.com/pallets/flask/blob/ @@ -112,8 +110,8 @@ def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: In the case of exceptions, request_finished_with_instana isn't called so we capture those cases here. """ - if hasattr(flask.g, "span") and flask.g.span is not None: - if len(argv) > 0 and argv[0] is not None: + if hasattr(flask.g, "span") and flask.g.span: + if len(argv) > 0 and argv[0]: span = flask.g.span span.record_exception(argv[0]) if SpanAttributes.HTTP_STATUS_CODE not in span.attributes: @@ -122,7 +120,7 @@ def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: flask.g.span.end() flask.g.span = None - if hasattr(flask.g, "token") and flask.g.token is not None: + if hasattr(flask.g, "token") and flask.g.token: context.detach(flask.g.token) flask.g.token = None From 415d13084610762defb65f5568fd10db3ec0e18e Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 31 Jul 2024 14:02:55 +0530 Subject: [PATCH 074/172] style: TypeHint fixes Signed-off-by: Varsha GS --- src/instana/instrumentation/flask/common.py | 12 ++++++++---- src/instana/instrumentation/flask/vanilla.py | 6 +++--- src/instana/instrumentation/flask/with_blinker.py | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index 3a72ed4a..cd966986 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -19,6 +19,7 @@ from instana.span.span import InstanaSpan from werkzeug.exceptions import HTTPException from flask.typing import ResponseReturnValue + from jinja2.environment import Template if signals_available: from werkzeug.datastructures.headers import Headers @@ -28,7 +29,10 @@ @wrapt.patch_function_wrapper('flask', 'templating._render') def render_with_instana( - wrapped: Callable[..., str], instance: Any, argv: Tuple, kwargs: Dict + wrapped: Callable[..., str], + instance: object, + argv: Tuple[flask.app.Flask, "Template", Dict[str, Any]], + kwargs: Dict[str, Any], ) -> str: # If we're not tracing, just return if not (hasattr(flask, "g") and hasattr(flask.g, "span")): @@ -58,8 +62,8 @@ def render_with_instana( def handle_user_exception_with_instana( wrapped: Callable[..., Union["HTTPException", "ResponseReturnValue"]], instance: flask.app.Flask, - argv: Tuple, - kwargs: Dict, + argv: Tuple[Exception], + kwargs: Dict[str, Any], ) -> Union["HTTPException", "ResponseReturnValue"]: # Call original and then try to do post processing response = wrapped(*argv, **kwargs) @@ -101,7 +105,7 @@ def handle_user_exception_with_instana( def extract_custom_headers( - span: "InstanaSpan", headers: Union[dict[str, Any], "Headers"], format: bool + span: "InstanaSpan", headers: Union[Dict[str, Any], "Headers"], format: bool ) -> None: if agent.options.extra_http_headers is None: return diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index b750ed68..9e21b033 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -5,7 +5,7 @@ import re import flask import wrapt -from typing import Any, Callable, Tuple, Dict +from typing import Callable, Tuple, Dict, Type, Union from opentelemetry.semconv.trace import SpanAttributes from opentelemetry import context, trace @@ -19,7 +19,7 @@ path_tpl_re = re.compile('<.*>') -def before_request_with_instana(*argv: Any, **kwargs: Any) -> None: +def before_request_with_instana() -> None: try: env = flask.request.environ span_context = tracer.extract(Format.HTTP_HEADERS, env) @@ -91,7 +91,7 @@ def after_request_with_instana( return response -def teardown_request_with_instana(*argv: Any, **kwargs: Any) -> None: +def teardown_request_with_instana(*argv: Union[Exception, Type[Exception]]) -> None: """ In the case of exceptions, after_request_with_instana isn't called so we capture those cases here. diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index cc57f877..211f2173 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -89,7 +89,7 @@ def request_finished_with_instana( def log_exception_with_instana( - sender: flask.app.Flask, exception: Any, **extra: Any + sender: flask.app.Flask, exception: Exception, **extra: Any ) -> None: if hasattr(flask.g, "span") and flask.g.span: span = flask.g.span From 9833870eb0a709f6e8a32278cf5166859a7ada27 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 31 Jul 2024 10:56:26 +0200 Subject: [PATCH 075/172] added unittest for readable span Signed-off-by: Cagri Yonca --- tests/span/test_readable_span.py | 91 ++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/span/test_readable_span.py diff --git a/tests/span/test_readable_span.py b/tests/span/test_readable_span.py new file mode 100644 index 00000000..4c4717f2 --- /dev/null +++ b/tests/span/test_readable_span.py @@ -0,0 +1,91 @@ +import time +from instana.span.readable_span import Event, ReadableSpan +from instana.span_context import SpanContext +from opentelemetry.trace.status import Status, StatusCode + + +def test_event() -> None: + name = "sample-event" + test_event = Event(name) + + assert test_event.name == name + assert not test_event.attributes + assert test_event.timestamp < time.time_ns() + + +def test_event_with_params() -> None: + name = "sample-event" + attributes = ["attribute"] + timestamp = time.time_ns() + test_event = Event(name, attributes, timestamp) + + assert test_event.name == name + assert test_event.attributes == attributes + assert test_event.timestamp == timestamp + + +def test_readablespan( + span_context: SpanContext, + trace_id: int, + span_id: int, +) -> None: + span_name = "test-span" + timestamp = time.time_ns() + span = ReadableSpan(span_name, span_context) + + assert span is not None + assert isinstance(span, ReadableSpan) + assert span.name == span_name + + span_context = span.context + assert isinstance(span_context, SpanContext) + assert span_context.trace_id == trace_id + assert span_context.span_id == span_id + + assert span.start_time + assert isinstance(span.start_time, int) + assert span.start_time > timestamp + assert not span.end_time + assert not span.attributes + assert not span.events + assert not span.parent_id + assert not span.duration + assert span.status + + assert not span.stack + assert span.synthetic is False + + +def test_readablespan_with_params( + span_context: SpanContext, +) -> None: + span_name = "test-span" + parent_id = "123456789" + start_time = time.time_ns() + end_time = time.time_ns() + attributes = {"key": "value"} + event_name = "event" + events = [Event(event_name, attributes, start_time)] + status = Status(StatusCode.OK) + stack = ["span-1", "span-2"] + span = ReadableSpan( + span_name, + span_context, + parent_id, + start_time, + end_time, + attributes, + events, + status, + stack, + ) + + assert span.name == span_name + assert span.parent_id == parent_id + assert span.start_time == start_time + assert span.end_time == end_time + assert span.attributes == attributes + assert span.events == events + assert span.status == status + assert span.duration == end_time - start_time + assert span.stack == stack From a05acab4ee85cb5b17c83b76a1944799070c00a2 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 31 Jul 2024 10:56:39 +0200 Subject: [PATCH 076/172] changed test folder structure Signed-off-by: Cagri Yonca --- tests/agent/test_host.py | 314 ++++++++++++++++++ .../test_base_span.py} | 0 .../test_event.py} | 0 .../test_registered_span.py} | 15 +- tests/{ => span}/test_span.py | 0 tests/{ => span}/test_span_sdk.py | 0 tests/test_span_context.py | 66 ---- tests/{ => util}/test_id_management.py | 0 tests/{ => util}/test_secrets.py | 0 tests/{ => util}/test_util.py | 0 10 files changed, 323 insertions(+), 72 deletions(-) create mode 100644 tests/agent/test_host.py rename tests/{test_span_base.py => span/test_base_span.py} (100%) rename tests/{test_span_event.py => span/test_event.py} (100%) rename tests/{test_span_registered.py => span/test_registered_span.py} (96%) rename tests/{ => span}/test_span.py (100%) rename tests/{ => span}/test_span_sdk.py (100%) delete mode 100644 tests/test_span_context.py rename tests/{ => util}/test_id_management.py (100%) rename tests/{ => util}/test_secrets.py (100%) rename tests/{ => util}/test_util.py (100%) diff --git a/tests/agent/test_host.py b/tests/agent/test_host.py new file mode 100644 index 00000000..032c2215 --- /dev/null +++ b/tests/agent/test_host.py @@ -0,0 +1,314 @@ +import datetime +import json +import logging +import os + +from unittest.mock import Mock, patch + +import pytest +import requests +from instana.agent.host import AnnounceData, HostAgent +from instana.collector.host import HostCollector +from instana.fsm import TheMachine +from instana.options import StandardOptions +from instana.recorder import StanRecorder +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext +from pytest import LogCaptureFixture + + +def test_init(): + with patch( + "instana.agent.base.BaseAgent.update_log_level" + ) as mock_update, patch.object(os, "getpid", return_value=12345): + agent = HostAgent() + assert not agent.announce_data + assert not agent.last_seen + assert not agent.last_fork_check + assert agent._boot_pid == 12345 + + mock_update.assert_called_once() + + assert isinstance(agent.options, StandardOptions) + assert isinstance(agent.collector, HostCollector) + assert isinstance(agent.machine, TheMachine) + + +def test_start(): + with patch("instana.collector.host.HostCollector.start") as mock_start: + agent = HostAgent() + agent.start() + mock_start.assert_called_once() + + +def test_handle_fork(): + with patch.object(HostAgent, "reset") as mock_reset: + agent = HostAgent() + agent.handle_fork() + mock_reset.assert_called_once() + + +def test_reset(): + with patch("instana.collector.host.HostCollector.shutdown") as mock_shutdown, patch( + "instana.fsm.TheMachine.reset" + ) as mock_reset: + agent = HostAgent() + agent.reset() + + assert not agent.last_seen + assert not agent.announce_data + + mock_shutdown.assert_called_once_with(report_final=False) + mock_reset.assert_called_once() + + +def test_is_timed_out(): + agent = HostAgent() + assert not agent.is_timed_out() + + agent.last_seen = datetime.datetime.now() - datetime.timedelta(minutes=5) + agent.can_send = True + assert agent.is_timed_out() + + +def test_can_send_test_env(): + agent = HostAgent() + with patch.dict("os.environ", {"INSTANA_TEST": "sample-data"}): + if "INSTANA_TEST" in os.environ: + assert agent.can_send() + + +def test_can_send(): + agent = HostAgent() + agent._boot_pid = 12345 + with patch.object(os, "getpid", return_value=12344), patch( + "instana.agent.host.HostAgent.handle_fork" + ) as mock_handle, patch.dict("os.environ", {}, clear=True): + agent.can_send() + assert agent._boot_pid == 12344 + mock_handle.assert_called_once() + + with patch.object(agent.machine.fsm, "current", "wait4init"): + assert agent.can_send() is True + + +def test_can_send_default(): + agent = HostAgent() + with patch.dict("os.environ", {}, clear=True): + assert not agent.can_send() + + +def test_set_from(): + agent = HostAgent() + sample_res_data = { + "secrets": {"matcher": "value-1", "list": ["value-2"]}, + "extraHeaders": ["value-3"], + "agentUuid": "value-4", + "pid": 1234, + } + agent.options.extra_http_headers = None + + agent.set_from(sample_res_data) + assert agent.options.secrets_matcher == "value-1" + assert agent.options.secrets_list == ["value-2"] + assert agent.options.extra_http_headers == ["value-3"] + + agent.options.extra_http_headers = ["value"] + agent.set_from(sample_res_data) + assert "value" in agent.options.extra_http_headers + + assert agent.announce_data.agentUuid == "value-4" + assert agent.announce_data.pid == 1234 + + +def test_get_from_structure(): + agent = HostAgent() + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + assert agent.get_from_structure() == {"e": 1234, "h": "value"} + + +def test_is_agent_listening( + caplog: LogCaptureFixture, +): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + with patch.object(requests.Session, "get", return_value=mock_response): + assert agent.is_agent_listening("sample", "1234") + + mock_response.status_code = 404 + with patch.object(requests.Session, "get", return_value=mock_response, clear=True): + assert not agent.is_agent_listening("sample", "1234") + + host = "localhost" + port = 123 + with patch.object(requests.Session, "get", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_listening(host, port) + assert f"Instana Host Agent not found on {host}:{port}" in caplog.messages + + +def test_announce( + caplog: LogCaptureFixture, +): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + response = json.loads(mock_response.content) + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = mock_response.content.encode("UTF-8") + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + + with patch.object(requests.Session, "put", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert f"announce: connection error ({type(Exception())})" in caplog.messages + + mock_response.content = json.dumps("key") + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert "announce: response payload has no fields: (key)" in caplog.messages + + mock_response.content = json.dumps({"key": "value"}) + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no pid: ({'key': 'value'})" + in caplog.messages + ) + + mock_response.content = json.dumps({"pid": "value"}) + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no agentUuid: ({'pid': 'value'})" + in caplog.messages + ) + + mock_response.status_code = 404 + with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + assert not agent.announce("sample-data") + assert "announce: response status code (404) is NOT 200" in caplog.messages + + +def test_log_message_to_host_agent( + caplog: LogCaptureFixture, +): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = "sample" + mock_datetime = datetime.datetime(2022, 1, 1, 12, 0, 0) + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.datetime" + ) as mock_date: + mock_date.now.return_value = mock_datetime + mock_date.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + agent.log_message_to_host_agent("sample") + assert agent.last_seen == mock_datetime + + with patch.object(requests.Session, "post", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.log_message_to_host_agent("sample") + assert ( + f"agent logging: connection error ({type(Exception())})" + in caplog.messages + ) + + +def test_is_agent_ready(caplog: LogCaptureFixture): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = {"key": "value"} + agent.AGENT_DATA_PATH = "sample_path" + agent.announce_data = AnnounceData(pid=1234, agentUuid="sample") + with patch.object(requests.Session, "head", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", return_value="localhost" + ): + assert agent.is_agent_ready() + with patch.object(requests.Session, "head", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_ready() + assert ( + f"is_agent_ready: connection error ({type(Exception())})" + in caplog.messages + ) + + +def test_report_data_payload( + span_context: SpanContext, + span_processor: StanRecorder, +): + agent = HostAgent() + span_name = "test-span" + span_1 = InstanaSpan(span_name, span_context, span_processor) + span_2 = InstanaSpan(span_name, span_context, span_processor) + payload = { + "spans": [span_1, span_2], + "profiles": ["profile-1", "profile-2"], + "metrics": { + "plugins": [ + {"data": "sample data"}, + ] + }, + } + sample_response = {"key": "value"} + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = sample_response + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__traces_url", return_value="localhost" + ), patch( + "instana.agent.host.HostAgent._HostAgent__profiles_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", return_value="localhost" + ): + test_response = agent.report_data_payload(payload) + assert isinstance(agent.last_seen, datetime.datetime) + assert test_response.content == sample_response + + +def test_diagnostics(caplog: LogCaptureFixture): + caplog.set_level(logging.WARNING, logger="instana") + + agent = HostAgent() + agent.diagnostics() + assert "====> Instana Python Language Agent Diagnostics <====" in caplog.messages + assert "----> Agent <----" in caplog.messages + assert f"is_agent_ready: {agent.is_agent_ready()}" in caplog.messages + assert f"is_timed_out: {agent.is_timed_out()}" in caplog.messages + assert "last_seen: None" in caplog.messages + + sample_date = datetime.datetime(2022, 7, 25, 14, 30, 0) + agent.last_seen = sample_date + agent.diagnostics() + assert "last_seen: 2022-07-25 14:30:00" in caplog.messages + assert "announce_data: None" in caplog.messages + + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + agent.diagnostics() + assert f"announce_data: {agent.announce_data.__dict__}" in caplog.messages + assert f"Options: {agent.options.__dict__}" in caplog.messages + assert "----> StateMachine <----" in caplog.messages + assert f"State: {agent.machine.fsm.current}" in caplog.messages + assert "----> Collector <----" in caplog.messages + assert f"Collector: {agent.collector}" in caplog.messages + assert f"ready_to_start: {agent.collector.ready_to_start}" in caplog.messages + assert "reporting_thread: None" in caplog.messages + assert f"report_interval: {agent.collector.report_interval}" in caplog.messages + assert "should_send_snapshot_data: True" in caplog.messages diff --git a/tests/test_span_base.py b/tests/span/test_base_span.py similarity index 100% rename from tests/test_span_base.py rename to tests/span/test_base_span.py diff --git a/tests/test_span_event.py b/tests/span/test_event.py similarity index 100% rename from tests/test_span_event.py rename to tests/span/test_event.py diff --git a/tests/test_span_registered.py b/tests/span/test_registered_span.py similarity index 96% rename from tests/test_span_registered.py rename to tests/span/test_registered_span.py index 3ba7bc6d..f381b1f3 100644 --- a/tests/test_span_registered.py +++ b/tests/span/test_registered_span.py @@ -396,11 +396,11 @@ def test_populate_exit_span_data_log( span_context: SpanContext, span_processor: StanRecorder ) -> None: span_name = service_name = "log" - span = InstanaSpan(span_name, span_context, span_processor) - reg_span = RegisteredSpan(span, None, service_name) + sample_span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(sample_span, None, service_name) excepted_text = "Houston, we have a problem!" - events = [ + sample_events = [ ( "test_populate_exit_span_data_log_event_with_message", { @@ -421,10 +421,13 @@ def test_populate_exit_span_data_log( ), ] - for event_name, attributes, timestamp in events: - span.add_event(event_name, attributes, timestamp) + for event_name, attributes, timestamp in sample_events: + sample_span.add_event(event_name, attributes, timestamp) - reg_span._populate_exit_span_data(span) + reg_span._populate_exit_span_data(sample_span) assert excepted_text == reg_span.data["log"]["message"] assert excepted_text == reg_span.data["log"]["parameters"] + + while sample_span._events: + sample_span._events.pop() diff --git a/tests/test_span.py b/tests/span/test_span.py similarity index 100% rename from tests/test_span.py rename to tests/span/test_span.py diff --git a/tests/test_span_sdk.py b/tests/span/test_span_sdk.py similarity index 100% rename from tests/test_span_sdk.py rename to tests/span/test_span_sdk.py diff --git a/tests/test_span_context.py b/tests/test_span_context.py deleted file mode 100644 index 517007f8..00000000 --- a/tests/test_span_context.py +++ /dev/null @@ -1,66 +0,0 @@ -# (c) Copyright IBM Corp. 2024 - -import pickle -from opentelemetry.trace.span import ( - DEFAULT_TRACE_OPTIONS, - DEFAULT_TRACE_STATE, - format_span_id, -) - -from instana.span_context import SpanContext -from instana.util.ids import generate_id - - -def test_span_context_defaults(): - trace_id = generate_id() - span_id = generate_id() - span_context = SpanContext( - trace_id=trace_id, - span_id=span_id, - is_remote=False, - ) - - assert isinstance(span_context, SpanContext) - assert span_context.trace_id == trace_id - assert span_context.span_id == span_id - assert span_context.trace_id != span_context.span_id - assert not span_context.is_remote - assert span_context.trace_flags == DEFAULT_TRACE_OPTIONS - assert span_context.trace_state == DEFAULT_TRACE_STATE - assert span_context.is_valid - assert span_context.level == 1 - assert not span_context.synthetic - assert span_context.trace_parent is None - assert span_context.instana_ancestor is None - assert span_context.long_trace_id is None - assert span_context.correlation_type is None - assert span_context.correlation_id is None - assert span_context.traceparent is None - assert span_context.tracestate is None - assert not span_context.suppression - assert repr(span_context) == f"SpanContext(trace_id=0x{format_span_id(trace_id)}, span_id=0x{format_span_id(span_id)}, trace_flags=0x{DEFAULT_TRACE_OPTIONS:02x}, trace_state={DEFAULT_TRACE_STATE!r}, is_remote=False, synthetic=False)" - - -def test_span_context_invalid(): - span_context = SpanContext( - trace_id=9999999999999999999999999999999999999999999999999999999999999999999999999999, - span_id=9, - is_remote=False, - ) - assert not span_context.is_valid - - -def test_span_context_pickle(): - trace_id = generate_id() - span_id = generate_id() - span_context = SpanContext( - trace_id=trace_id, - span_id=span_id, - is_remote=False, - ) - - span_context_binary = pickle.dumps(span_context) - span_context_pickle = pickle.loads(span_context_binary) - assert trace_id == span_context_pickle.trace_id - assert span_id == span_context_pickle.span_id - diff --git a/tests/test_id_management.py b/tests/util/test_id_management.py similarity index 100% rename from tests/test_id_management.py rename to tests/util/test_id_management.py diff --git a/tests/test_secrets.py b/tests/util/test_secrets.py similarity index 100% rename from tests/test_secrets.py rename to tests/util/test_secrets.py diff --git a/tests/test_util.py b/tests/util/test_util.py similarity index 100% rename from tests/test_util.py rename to tests/util/test_util.py From 16b8326bb794abccc4e6a6f47f434a2c200f3a94 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 4 Jul 2024 16:28:04 +0200 Subject: [PATCH 077/172] fix(tests): Add pytest-mock as test requirement. Signed-off-by: Paulo Vital --- tests/test_tracer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index ba6ec778..62f3bc15 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,5 +1,7 @@ # (c) Copyright IBM Corp. 2024 +from opentelemetry.trace import set_span_in_context +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID import pytest from instana.agent.test import TestAgent from instana.recorder import StanRecorder From 3aff66c40d4842084cc9c6b61125fd836ac7d042 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 21 Mar 2024 22:08:27 +0100 Subject: [PATCH 078/172] fix: Remove not necessary async_tracer and tornado_tracer. Signed-off-by: Paulo Vital --- src/instana/singletons.py | 8 -------- src/instana/util/traceutils.py | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/instana/singletons.py b/src/instana/singletons.py index 0b22e903..0d04f22a 100644 --- a/src/instana/singletons.py +++ b/src/instana/singletons.py @@ -10,7 +10,6 @@ agent = None tracer = None -async_tracer = None profiler = None span_recorder = None @@ -104,13 +103,6 @@ def set_agent(new_agent): # Creates a tracer from the global tracer provider tracer = trace.get_tracer("instana.tracer") -async_tracer = trace.get_tracer("instana.async.tracer") -tornado_tracer = None - - -def setup_tornado_tracer(): - global tornado_tracer - tornado_tracer = trace.get_tracer("instana.tornado.tracer") def get_tracer(): diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index 3d82da2d..7db865cc 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -4,7 +4,7 @@ from typing import Optional, Tuple from instana.log import logger -from instana.singletons import agent, tracer, async_tracer, tornado_tracer +from instana.singletons import agent, tracer from instana.span.span import InstanaSpan, get_current_span from instana.tracer import InstanaTracer From 3d81697ce708bc41208db94b3befd298fca3eaaf Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 27 May 2024 16:37:27 +0200 Subject: [PATCH 079/172] style: add type hints in the traceutils.py file. Signed-off-by: Paulo Vital --- src/instana/util/traceutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index 7db865cc..f7a35af3 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -9,7 +9,7 @@ from instana.tracer import InstanaTracer -def extract_custom_headers(tracing_span, headers): +def extract_custom_headers(tracing_span, headers) -> None: try: for custom_header in agent.options.extra_http_headers: # Headers are in the following format: b'x-header-1' @@ -46,5 +46,5 @@ def get_tracer_tuple() -> ( return (None, None, None) -def tracing_is_off(): +def tracing_is_off() -> bool: return not (bool(get_active_tracer()) or agent.options.allow_exit_as_root) From 3652b8e86dfdb8267c73f012cf009f17d6830396 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 27 May 2024 16:56:22 +0200 Subject: [PATCH 080/172] refactor: urllib3 instrumentation. Signed-off-by: Paulo Vital Co-authored-by: Varsha GS --- src/instana/instrumentation/urllib3.py | 68 +++++++++++++------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/instana/instrumentation/urllib3.py b/src/instana/instrumentation/urllib3.py index 12542bc5..612da188 100644 --- a/src/instana/instrumentation/urllib3.py +++ b/src/instana/instrumentation/urllib3.py @@ -2,33 +2,33 @@ # (c) Copyright Instana Inc. 2017 -import opentracing -import opentracing.ext.tags as ext +from typing import Dict import wrapt +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import set_span_in_context -from ..log import logger -from ..singletons import agent -from ..util.traceutils import get_tracer_tuple, tracing_is_off -from ..util.secrets import strip_secrets_from_query +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent +from instana.span import InstanaSpan +from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import urllib3 - - def extract_custom_headers(span, headers): + def _extract_custom_headers(span: InstanaSpan, headers: Dict) -> None: if agent.options.extra_http_headers is None: return + try: for custom_header in agent.options.extra_http_headers: if custom_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[custom_header]) - + span.set_attribute(f"http.header.{custom_header}", headers[custom_header]) except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) - + logger.debug("urllib3 _extract_custom_headers error: ", exc_info=True) - def collect(instance, args, kwargs): - """ Build and return a fully qualified URL for this request """ + def _collect_kvs(instance, args, kwargs) -> Dict: kvs = dict() try: kvs['host'] = instance.host @@ -56,55 +56,55 @@ def collect(instance, args, kwargs): else: kvs['url'] = 'http://%s:%d%s' % (kvs['host'], kvs['port'], kvs['path']) except Exception: - logger.debug("urllib3 collect error", exc_info=True) + logger.debug("urllib3 _collect_kvs error: ", exc_info=True) return kvs else: return kvs - - def collect_response(scope, response): + def collect_response(span, response): try: - scope.span.set_tag(ext.HTTP_STATUS_CODE, response.status) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) - extract_custom_headers(scope.span, response.headers) + _extract_custom_headers(span, response.headers) if 500 <= response.status: - scope.span.mark_as_errored() + span.mark_as_errored() except Exception: - logger.debug("collect_response", exc_info=True) + logger.debug("urllib3 collect_response error: ", exc_info=True) @wrapt.patch_function_wrapper('urllib3', 'HTTPConnectionPool.urlopen') def urlopen_with_instana(wrapped, instance, args, kwargs): - tracer, parent_span, operation_name = get_tracer_tuple() + tracer, parent_span, span_name = get_tracer_tuple() + # If we're not tracing, just return; boto3 has it's own visibility - if (tracing_is_off() or (operation_name == 'boto3')): + if tracing_is_off() or (span_name == 'boto3'): return wrapped(*args, **kwargs) - with tracer.start_active_span("urllib3", child_of=parent_span) as scope: + parent_context = set_span_in_context(parent_span) + + with tracer.start_as_current_span("urllib3", context=parent_context) as span: try: - kvs = collect(instance, args, kwargs) + kvs = _collect_kvs(instance, args, kwargs) if 'url' in kvs: - scope.span.set_tag(ext.HTTP_URL, kvs['url']) + span.set_attribute(SpanAttributes.HTTP_URL, kvs['url']) if 'query' in kvs: - scope.span.set_tag("http.params", kvs['query']) + span.set_attribute("http.params", kvs['query']) if 'method' in kvs: - scope.span.set_tag(ext.HTTP_METHOD, kvs['method']) - + span.set_attribute(SpanAttributes.HTTP_METHOD, kvs['method']) if 'headers' in kwargs: - extract_custom_headers(scope.span, kwargs['headers']) - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, kwargs['headers']) + _extract_custom_headers(span, kwargs['headers']) + tracer.inject(span.context, Format.HTTP_HEADERS, kwargs['headers']) response = wrapped(*args, **kwargs) - collect_response(scope, response) + collect_response(span, response) return response except Exception as e: - scope.span.mark_as_errored({'message': e}) + span.record_exception({'message': e}) raise - logger.debug("Instrumenting urllib3") except ImportError: pass From 4e4e47dc182f0f19404eb72dcaf5f7d1408732f2 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 22 Jul 2024 16:12:34 +0530 Subject: [PATCH 081/172] fix(urllib3): pass the parent span context to start_as_current_span() Signed-off-by: Varsha GS --- src/instana/instrumentation/urllib3.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/instana/instrumentation/urllib3.py b/src/instana/instrumentation/urllib3.py index 612da188..5b3b1f00 100644 --- a/src/instana/instrumentation/urllib3.py +++ b/src/instana/instrumentation/urllib3.py @@ -4,13 +4,14 @@ from typing import Dict import wrapt + from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import set_span_in_context from instana.log import logger from instana.propagators.format import Format from instana.singletons import agent -from instana.span import InstanaSpan +from instana.span.span import InstanaSpan from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import get_tracer_tuple, tracing_is_off @@ -81,9 +82,11 @@ def urlopen_with_instana(wrapped, instance, args, kwargs): if tracing_is_off() or (span_name == 'boto3'): return wrapped(*args, **kwargs) - parent_context = set_span_in_context(parent_span) - - with tracer.start_as_current_span("urllib3", context=parent_context) as span: + parent_context = parent_span.get_span_context() + + with tracer.start_as_current_span( + "urllib3", span_context=parent_context + ) as span: try: kvs = _collect_kvs(instance, args, kwargs) if 'url' in kvs: From aafd00db96c9e77f2df32fe0b4472efb5bfe24a0 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 9 Jul 2024 11:19:13 +0200 Subject: [PATCH 082/172] style: format instrumentation/urllib3.py Add type hints to methods and used ruff (vscode) to: - Black-compatible code formatting. - fix all auto-fixable violations, like unused imports. - isort-compatible import sorting. Signed-off-by: Paulo Vital --- src/instana/instrumentation/urllib3.py | 107 +++++++++++++++---------- 1 file changed, 66 insertions(+), 41 deletions(-) diff --git a/src/instana/instrumentation/urllib3.py b/src/instana/instrumentation/urllib3.py index 5b3b1f00..00ee7648 100644 --- a/src/instana/instrumentation/urllib3.py +++ b/src/instana/instrumentation/urllib3.py @@ -2,67 +2,83 @@ # (c) Copyright Instana Inc. 2017 -from typing import Dict -import wrapt +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union +import wrapt from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import set_span_in_context from instana.log import logger from instana.propagators.format import Format from instana.singletons import agent -from instana.span.span import InstanaSpan from instana.util.secrets import strip_secrets_from_query from instana.util.traceutils import get_tracer_tuple, tracing_is_off +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + try: import urllib3 - def _extract_custom_headers(span: InstanaSpan, headers: Dict) -> None: + def _extract_custom_headers(span: "InstanaSpan", headers: Dict[str, Any]) -> None: if agent.options.extra_http_headers is None: return try: for custom_header in agent.options.extra_http_headers: if custom_header in headers: - span.set_attribute(f"http.header.{custom_header}", headers[custom_header]) + span.set_attribute( + f"http.header.{custom_header}", headers[custom_header] + ) except Exception: logger.debug("urllib3 _extract_custom_headers error: ", exc_info=True) - def _collect_kvs(instance, args, kwargs) -> Dict: + def _collect_kvs( + instance: Union[ + urllib3.connectionpool.HTTPConnectionPool, + urllib3.connectionpool.HTTPSConnectionPool, + ], + args: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], + ) -> Dict[str, Any]: kvs = dict() try: - kvs['host'] = instance.host - kvs['port'] = instance.port + kvs["host"] = instance.host + kvs["port"] = instance.port - if args is not None and len(args) == 2: - kvs['method'] = args[0] - kvs['path'] = args[1] + if args and len(args) == 2: + kvs["method"] = args[0] + kvs["path"] = args[1] else: - kvs['method'] = kwargs.get('method') - kvs['path'] = kwargs.get('path') - if kvs['path'] is None: - kvs['path'] = kwargs.get('url') + kvs["method"] = kwargs.get("method") + kvs["path"] = ( + kwargs.get("path") if kwargs.get("path") else kwargs.get("url") + ) # Strip any secrets from potential query params - if kvs.get('path') is not None and ('?' in kvs['path']): - parts = kvs['path'].split('?') - kvs['path'] = parts[0] + if kvs.get("path") and ("?" in kvs["path"]): + parts = kvs["path"].split("?") + kvs["path"] = parts[0] if len(parts) == 2: - kvs['query'] = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, - agent.options.secrets_list) - - if type(instance) is urllib3.connectionpool.HTTPSConnectionPool: - kvs['url'] = 'https://%s:%d%s' % (kvs['host'], kvs['port'], kvs['path']) + kvs["query"] = strip_secrets_from_query( + parts[1], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + + url = kvs["host"] + ":" + str(kvs["port"]) + kvs["path"] + if isinstance(instance, urllib3.connectionpool.HTTPSConnectionPool): + kvs["url"] = f"https://{url}" else: - kvs['url'] = 'http://%s:%d%s' % (kvs['host'], kvs['port'], kvs['path']) + kvs["url"] = f"http://{url}" except Exception: logger.debug("urllib3 _collect_kvs error: ", exc_info=True) return kvs else: return kvs - def collect_response(span, response): + def collect_response( + span: "InstanaSpan", response: urllib3.response.HTTPResponse + ) -> None: try: span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) @@ -73,31 +89,40 @@ def collect_response(span, response): except Exception: logger.debug("urllib3 collect_response error: ", exc_info=True) - - @wrapt.patch_function_wrapper('urllib3', 'HTTPConnectionPool.urlopen') - def urlopen_with_instana(wrapped, instance, args, kwargs): + @wrapt.patch_function_wrapper("urllib3", "HTTPConnectionPool.urlopen") + def urlopen_with_instana( + wrapped: Callable[ + ..., Union[urllib3.HTTPConnectionPool, urllib3.HTTPSConnectionPool] + ], + instance: Union[ + urllib3.connectionpool.HTTPConnectionPool, + urllib3.connectionpool.HTTPSConnectionPool, + ], + args: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], + ) -> urllib3.response.HTTPResponse: tracer, parent_span, span_name = get_tracer_tuple() # If we're not tracing, just return; boto3 has it's own visibility - if tracing_is_off() or (span_name == 'boto3'): + if tracing_is_off() or (span_name == "boto3"): return wrapped(*args, **kwargs) - parent_context = parent_span.get_span_context() + parent_context = parent_span.get_span_context() if parent_span else None with tracer.start_as_current_span( "urllib3", span_context=parent_context ) as span: try: kvs = _collect_kvs(instance, args, kwargs) - if 'url' in kvs: - span.set_attribute(SpanAttributes.HTTP_URL, kvs['url']) - if 'query' in kvs: - span.set_attribute("http.params", kvs['query']) - if 'method' in kvs: - span.set_attribute(SpanAttributes.HTTP_METHOD, kvs['method']) - if 'headers' in kwargs: - _extract_custom_headers(span, kwargs['headers']) - tracer.inject(span.context, Format.HTTP_HEADERS, kwargs['headers']) + if "url" in kvs: + span.set_attribute(SpanAttributes.HTTP_URL, kvs["url"]) + if "query" in kvs: + span.set_attribute("http.params", kvs["query"]) + if "method" in kvs: + span.set_attribute(SpanAttributes.HTTP_METHOD, kvs["method"]) + if "headers" in kwargs: + _extract_custom_headers(span, kwargs["headers"]) + tracer.inject(span.context, Format.HTTP_HEADERS, kwargs["headers"]) response = wrapped(*args, **kwargs) @@ -105,7 +130,7 @@ def urlopen_with_instana(wrapped, instance, args, kwargs): return response except Exception as e: - span.record_exception({'message': e}) + span.record_exception(e) raise logger.debug("Instrumenting urllib3") From db7c82e334fcd49a617a8a003633fc84ac4ab3c4 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 1 Aug 2024 16:30:42 +0200 Subject: [PATCH 083/172] tests(urllib3): adapt tests to OTel usage. Signed-off-by: Paulo Vital --- tests/clients/test_urllib3.py | 1135 ++++++++++++++++++--------------- 1 file changed, 632 insertions(+), 503 deletions(-) diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index de8337de..1347d0a1 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -1,311 +1,376 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +import logging +import sys from multiprocessing.pool import ThreadPool from time import sleep -import unittest +from typing import TYPE_CHECKING, Generator -import urllib3 +import pytest import requests - -import tests.apps.flask_app -from ..helpers import testenv +import urllib3 +from instana.instrumentation.urllib3 import ( + _collect_kvs as collect_kvs, + _extract_custom_headers as extract_custom_headers, + collect_response, +) from instana.singletons import agent, tracer +import tests.apps.flask_app # noqa: F401 +from tests.helpers import testenv + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from pytest import LogCaptureFixture + -class TestUrllib3(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ +class TestUrllib3: + @pytest.fixture(autouse=True) + def _setup(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run self.http = urllib3.PoolManager() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + yield + # teardown + # Ensure that allow_exit_as_root has the default value""" agent.options.allow_exit_as_root = False - def test_vanilla_requests(self): - r = self.http.request('GET', testenv["wsgi_server"] + '/') - self.assertEqual(r.status, 200) + def test_vanilla_requests(self) -> None: + r = self.http.request("GET", testenv["wsgi_server"] + "/") + assert r.status == 200 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - def test_parallel_requests(self): + def test_parallel_requests(self) -> None: http_pool_5 = urllib3.PoolManager(num_pools=5) def task(num): - r = http_pool_5.request('GET', testenv["wsgi_server"] + '/', fields={'num': num}) - return r + r = http_pool_5.request( + "GET", testenv["wsgi_server"] + "/", fields={"num": num} + ) + return r with ThreadPool(processes=5) as executor: # iterate over results as they become available for result in executor.map(task, (1, 2, 3, 4, 5)): - self.assertEqual(result.status, 200) + assert result.status == 200 spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) - nums = map(lambda s: s.data['http']['params'].split('=')[1], spans) - self.assertEqual(set(nums), set(('1', '2', '3', '4', '5'))) - - def test_customers_setup_zd_26466(self): - def make_request(u=None): - sleep(10) - x = requests.get(testenv["wsgi_server"] + '/') - sleep(10) - return x.status_code + assert len(spans) == 5 + nums = map(lambda s: s.data["http"]["params"].split("=")[1], spans) + assert set(nums) == set(("1", "2", "3", "4", "5")) + + @pytest.mark.skipif( + sys.platform == "darwin", + reason="Avoiding ConnectionError when calling multi processes of Flask app.", + ) + def test_customers_setup_zd_26466(self) -> None: + def make_request(u=None) -> int: + sleep(10) + x = requests.get(testenv["wsgi_server"] + "/") + sleep(10) + return x.status_code status = make_request() - #print(f'request made outside threadpool, instana should instrument - status: {status}') + assert status == 200 + # print(f'request made outside threadpool, instana should instrument - status: {status}') threadpool_size = 15 pool = ThreadPool(processes=threadpool_size) res = pool.map(make_request, [u for u in range(threadpool_size)]) - #print(f'requests made within threadpool, instana does not instrument - statuses: {res}') + # print(f'requests made within threadpool, instana does not instrument - statuses: {res}') spans = self.recorder.queued_spans() - self.assertEqual(16, len(spans)) - + assert len(spans) == 16 def test_get_request(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["wsgi_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack + + # urllib3 + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + def test_get_request_https(self): + with tracer.start_as_current_span("test"): + r = self.http.request("GET", "https://httpbin.org/robots.txt") + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + urllib3_span = spans[0] + test_span = spans[1] + + assert r + assert r.status == 200 + + # Same traceId + assert test_span.t == urllib3_span.t + + # Parent relationships + assert urllib3_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not urllib3_span.ec # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == "https://httpbin.org:443/robots.txt" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_get_request_as_root_exit_span(self): agent.options.allow_exit_as_root = True - r = self.http.request('GET', testenv["wsgi_server"] + '/') + r = self.http.request("GET", testenv["wsgi_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 wsgi_span = spans[0] urllib3_span = spans[1] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, None) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert not urllib3_span.p + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_get_request_with_query(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/?one=1&two=2') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["wsgi_server"] + "/?one=1&two=2") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertTrue(urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] ) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_get_request_with_alt_query(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/', fields={'one': '1', 'two': 2}) + with tracer.start_as_current_span("test"): + r = self.http.request( + "GET", testenv["wsgi_server"] + "/", fields={"one": "1", "two": 2} + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertTrue(urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] ) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_put_request(self): - with tracer.start_active_span('test'): - r = self.http.request('PUT', testenv["wsgi_server"] + '/notfound') + with tracer.start_as_current_span("test"): + r = self.http.request("PUT", testenv["wsgi_server"] + "/notfound") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(404, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 404 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/notfound', wsgi_span.data["http"]["url"]) - self.assertEqual('PUT', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/notfound" + assert wsgi_span.data["http"]["method"] == "PUT" + assert wsgi_span.data["http"]["status"] == 404 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/notfound", urllib3_span.data["http"]["url"]) - self.assertEqual("PUT", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 404 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/notfound" + assert urllib3_span.data["http"]["method"] == "PUT" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_301_redirect(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/301') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["wsgi_server"] + "/301") spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert len(spans) == 5 wsgi_span2 = spans[0] urllib3_span2 = spans[1] @@ -313,71 +378,75 @@ def test_301_redirect(self): urllib3_span1 = spans[3] test_span = spans[4] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span1.t) - self.assertEqual(traceId, wsgi_span1.t) - self.assertEqual(traceId, urllib3_span2.t) - self.assertEqual(traceId, wsgi_span2.t) + assert urllib3_span1.t == traceId + assert wsgi_span1.t == traceId + assert urllib3_span2.t == traceId + assert wsgi_span2.t == traceId # Parent relationships - self.assertEqual(urllib3_span1.p, test_span.s) - self.assertEqual(wsgi_span1.p, urllib3_span1.s) - self.assertEqual(urllib3_span2.p, test_span.s) - self.assertEqual(wsgi_span2.p, urllib3_span2.s) + assert urllib3_span1.p == test_span.s + assert wsgi_span1.p == urllib3_span1.s + assert urllib3_span2.p == test_span.s + assert wsgi_span2.p == urllib3_span2.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span1.ec) - self.assertIsNone(wsgi_span1.ec) - self.assertIsNone(urllib3_span2.ec) - self.assertIsNone(wsgi_span2.ec) + assert not test_span.ec + assert not urllib3_span1.ec + assert not wsgi_span1.ec + assert not urllib3_span2.ec + assert not wsgi_span2.ec # wsgi - self.assertEqual("wsgi", wsgi_span1.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span1.data["http"]["host"]) - self.assertEqual('/', wsgi_span1.data["http"]["url"]) - self.assertEqual('GET', wsgi_span1.data["http"]["method"]) - self.assertEqual(200, wsgi_span1.data["http"]["status"]) - self.assertIsNone(wsgi_span1.data["http"]["error"]) - self.assertIsNone(wsgi_span1.stack) - - self.assertEqual("wsgi", wsgi_span2.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span2.data["http"]["host"]) - self.assertEqual('/301', wsgi_span2.data["http"]["url"]) - self.assertEqual('GET', wsgi_span2.data["http"]["method"]) - self.assertEqual(301, wsgi_span2.data["http"]["status"]) - self.assertIsNone(wsgi_span2.data["http"]["error"]) - self.assertIsNone(wsgi_span2.stack) + assert wsgi_span1.n == "wsgi" + assert wsgi_span1.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span1.data["http"]["url"] == "/" + assert wsgi_span1.data["http"]["method"] == "GET" + assert wsgi_span1.data["http"]["status"] == 200 + assert not wsgi_span1.data["http"]["error"] + assert not wsgi_span1.stack + + assert wsgi_span2.n == "wsgi" + assert wsgi_span2.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span2.data["http"]["url"] == "/301" + assert wsgi_span2.data["http"]["method"] == "GET" + assert wsgi_span2.data["http"]["status"] == 301 + assert not wsgi_span2.data["http"]["error"] + assert not wsgi_span2.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span1.n) - self.assertEqual(200, urllib3_span1.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span1.data["http"]["url"]) - self.assertEqual("GET", urllib3_span1.data["http"]["method"]) - self.assertIsNotNone(urllib3_span1.stack) - self.assertTrue(type(urllib3_span1.stack) is list) - self.assertTrue(len(urllib3_span1.stack) > 1) - - self.assertEqual("urllib3", urllib3_span2.n) - self.assertEqual(301, urllib3_span2.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/301", urllib3_span2.data["http"]["url"]) - self.assertEqual("GET", urllib3_span2.data["http"]["method"]) - self.assertIsNotNone(urllib3_span2.stack) - self.assertTrue(type(urllib3_span2.stack) is list) - self.assertTrue(len(urllib3_span2.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span1.n == "urllib3" + assert urllib3_span1.data["http"]["status"] == 200 + assert urllib3_span1.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span1.data["http"]["method"] == "GET" + assert urllib3_span1.stack + assert isinstance(urllib3_span1.stack, list) + assert len(urllib3_span1.stack) > 1 + + assert urllib3_span2.n == "urllib3" + assert urllib3_span2.data["http"]["status"] == 301 + assert urllib3_span2.data["http"]["url"] == testenv["wsgi_server"] + "/301" + assert urllib3_span2.data["http"]["method"] == "GET" + assert urllib3_span2.stack + assert isinstance(urllib3_span2.stack, list) + assert len(urllib3_span2.stack) > 1 def test_302_redirect(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/302') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["wsgi_server"] + "/302") spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert len(spans) == 5 wsgi_span2 = spans[0] urllib3_span2 = spans[1] @@ -385,117 +454,123 @@ def test_302_redirect(self): urllib3_span1 = spans[3] test_span = spans[4] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span1.t) - self.assertEqual(traceId, wsgi_span1.t) - self.assertEqual(traceId, urllib3_span2.t) - self.assertEqual(traceId, wsgi_span2.t) + assert urllib3_span1.t == traceId + assert wsgi_span1.t == traceId + assert urllib3_span2.t == traceId + assert wsgi_span2.t == traceId # Parent relationships - self.assertEqual(urllib3_span1.p, test_span.s) - self.assertEqual(wsgi_span1.p, urllib3_span1.s) - self.assertEqual(urllib3_span2.p, test_span.s) - self.assertEqual(wsgi_span2.p, urllib3_span2.s) + assert urllib3_span1.p == test_span.s + assert wsgi_span1.p == urllib3_span1.s + assert urllib3_span2.p == test_span.s + assert wsgi_span2.p == urllib3_span2.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span1.ec) - self.assertIsNone(wsgi_span1.ec) - self.assertIsNone(urllib3_span2.ec) - self.assertIsNone(wsgi_span2.ec) + assert not test_span.ec + assert not urllib3_span1.ec + assert not wsgi_span1.ec + assert not urllib3_span2.ec + assert not wsgi_span2.ec # wsgi - self.assertEqual("wsgi", wsgi_span1.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span1.data["http"]["host"]) - self.assertEqual('/', wsgi_span1.data["http"]["url"]) - self.assertEqual('GET', wsgi_span1.data["http"]["method"]) - self.assertEqual(200, wsgi_span1.data["http"]["status"]) - self.assertIsNone(wsgi_span1.data["http"]["error"]) - self.assertIsNone(wsgi_span1.stack) - - self.assertEqual("wsgi", wsgi_span2.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span2.data["http"]["host"]) - self.assertEqual('/302', wsgi_span2.data["http"]["url"]) - self.assertEqual('GET', wsgi_span2.data["http"]["method"]) - self.assertEqual(302, wsgi_span2.data["http"]["status"]) - self.assertIsNone(wsgi_span2.data["http"]["error"]) - self.assertIsNone(wsgi_span2.stack) + assert wsgi_span1.n == "wsgi" + assert wsgi_span1.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span1.data["http"]["url"] == "/" + assert wsgi_span1.data["http"]["method"] == "GET" + assert wsgi_span1.data["http"]["status"] == 200 + assert not wsgi_span1.data["http"]["error"] + assert not wsgi_span1.stack + + assert wsgi_span2.n == "wsgi" + assert wsgi_span2.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span2.data["http"]["url"] == "/302" + assert wsgi_span2.data["http"]["method"] == "GET" + assert wsgi_span2.data["http"]["status"] == 302 + assert not wsgi_span2.data["http"]["error"] + assert not wsgi_span2.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span1.n) - self.assertEqual(200, urllib3_span1.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span1.data["http"]["url"]) - self.assertEqual("GET", urllib3_span1.data["http"]["method"]) - self.assertIsNotNone(urllib3_span1.stack) - self.assertTrue(type(urllib3_span1.stack) is list) - self.assertTrue(len(urllib3_span1.stack) > 1) - - self.assertEqual("urllib3", urllib3_span2.n) - self.assertEqual(302, urllib3_span2.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/302", urllib3_span2.data["http"]["url"]) - self.assertEqual("GET", urllib3_span2.data["http"]["method"]) - self.assertIsNotNone(urllib3_span2.stack) - self.assertTrue(type(urllib3_span2.stack) is list) - self.assertTrue(len(urllib3_span2.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span1.n == "urllib3" + assert urllib3_span1.data["http"]["status"] == 200 + assert urllib3_span1.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span1.data["http"]["method"] == "GET" + assert urllib3_span1.stack + assert isinstance(urllib3_span1.stack, list) + assert len(urllib3_span1.stack) > 1 + + assert urllib3_span2.n == "urllib3" + assert urllib3_span2.data["http"]["status"] == 302 + assert urllib3_span2.data["http"]["url"] == testenv["wsgi_server"] + "/302" + assert urllib3_span2.data["http"]["method"] == "GET" + assert urllib3_span2.stack + assert isinstance(urllib3_span2.stack, list) + assert len(urllib3_span2.stack) > 1 def test_5xx_request(self): - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/504') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["wsgi_server"] + "/504") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(504, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 504 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert urllib3_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert wsgi_span.ec == 1 # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/504', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(504, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/504" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 504 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(504, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/504", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 504 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/504" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_exception_logging(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: - r = self.http.request('GET', testenv["wsgi_server"] + '/exception') + r = self.http.request("GET", testenv["wsgi_server"] + "/exception") except Exception: pass @@ -511,352 +586,406 @@ def test_exception_logging(self): # we will just discard the optional log span if present # Without blinker, our instrumentation logs roughly the same exception data onto the # already existing wsgi span. Which we validate in this TC if present. - self.assertIn(len(spans), (3, 4)) + assert len(spans) in (3, 4) + with_blinker = len(spans) == 3 if not with_blinker: spans = spans[1:] wsgi_span, urllib3_span, test_span = spans - self.assertTrue(r) - self.assertEqual(500, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 500 + # assert not tracer.active_span # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert urllib3_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, wsgi_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert wsgi_span.ec == 1 # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/exception', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(500, wsgi_span.data["http"]["status"]) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/exception" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 500 if with_blinker: - self.assertEqual('fake error', wsgi_span.data["http"]["error"]) + assert wsgi_span.data["http"]["error"] == "fake error" else: - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/exception", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 500 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/exception" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_client_error(self): r = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): try: - r = self.http.request('GET', 'http://doesnotexist.asdf:5000/504', - retries=False, - timeout=urllib3.Timeout(connect=0.5, read=0.5)) + r = self.http.request( + "GET", + "http://doesnotexist.asdf:5000/504", + retries=False, + timeout=urllib3.Timeout(connect=0.5, read=0.5), + ) except Exception: pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 urllib3_span = spans[0] test_span = spans[1] - self.assertIsNone(r) + assert not r # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) + assert urllib3_span.p == test_span.s # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span.t) + assert urllib3_span.t == traceId - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertIsNone(urllib3_span.data["http"]["status"]) - self.assertEqual("http://doesnotexist.asdf:5000/504", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert not urllib3_span.data["http"]["status"] + assert urllib3_span.data["http"]["url"] == "http://doesnotexist.asdf:5000/504" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 2 - def test_requestspkg_get(self): + def test_requests_pkg_get(self): self.recorder.clear_spans() - with tracer.start_active_span('test'): - r = requests.get(testenv["wsgi_server"] + '/', timeout=2) + with tracer.start_as_current_span("test"): + r = requests.get(testenv["wsgi_server"] + "/", timeout=2) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status_code) - self.assertIsNone(tracer.active_span) + assert r + assert r.status_code == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - def test_requestspkg_get_with_custom_headers(self): + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + def test_requests_pkg_get_with_custom_headers(self): my_custom_headers = dict() - my_custom_headers['X-PGL-1'] = '1' + my_custom_headers["X-PGL-1"] = "1" - with tracer.start_active_span('test'): - r = requests.get(testenv["wsgi_server"] + '/', timeout=2, headers=my_custom_headers) + with tracer.start_as_current_span("test"): + r = requests.get( + testenv["wsgi_server"] + "/", timeout=2, headers=my_custom_headers + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status_code) - self.assertIsNone(tracer.active_span) + assert r + assert r.status_code == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - def test_requestspkg_put(self): - with tracer.start_active_span('test'): - r = requests.put(testenv["wsgi_server"] + '/notfound') + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + def test_requests_pkg_put(self): + with tracer.start_as_current_span("test"): + r = requests.put(testenv["wsgi_server"] + "/notfound") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertEqual(404, r.status_code) - self.assertIsNone(tracer.active_span) + assert r.status_code == 404 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/notfound', wsgi_span.data["http"]["url"]) - self.assertEqual('PUT', wsgi_span.data["http"]["method"]) - self.assertEqual(404, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/notfound" + assert wsgi_span.data["http"]["method"] == "PUT" + assert wsgi_span.data["http"]["status"] == 404 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(404, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/notfound", urllib3_span.data["http"]["url"]) - self.assertEqual("PUT", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 404 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/notfound" + assert urllib3_span.data["http"]["method"] == "PUT" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 def test_response_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_active_span('test'): - r = self.http.request('GET', testenv["wsgi_server"] + '/response_headers') + with tracer.start_as_current_span("test"): + r = self.http.request("GET", testenv["wsgi_server"] + "/response_headers") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/response_headers', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/response_headers" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/response_headers", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - self.assertIn("X-Capture-This", urllib3_span.data["http"]["header"]) - self.assertEqual("Ok", urllib3_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", urllib3_span.data["http"]["header"]) - self.assertEqual("Ok too", urllib3_span.data["http"]["header"]["X-Capture-That"]) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert ( + urllib3_span.data["http"]["url"] + == testenv["wsgi_server"] + "/response_headers" + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + assert "X-Capture-This" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-This"] == "Ok" + assert "X-Capture-That" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-That"] == "Ok too" agent.options.extra_http_headers = original_extra_http_headers def test_request_header_capture(self): original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] request_headers = { "X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too", } - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): r = self.http.request( "GET", testenv["wsgi_server"] + "/", headers=request_headers ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(r) - self.assertEqual(200, r.status) - self.assertIsNone(tracer.active_span) + assert r + assert r.status == 200 + # assert not tracer.active_span # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv["wsgi_port"]), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( + testenv["wsgi_port"] + ) + assert wsgi_span.data["http"]["url"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == 200 + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - self.assertIn("X-Capture-This-Too", urllib3_span.data["http"]["header"]) - self.assertEqual("this too", urllib3_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", urllib3_span.data["http"]["header"]) - self.assertEqual("that too", urllib3_span.data["http"]["header"]["X-Capture-That-Too"]) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert isinstance(urllib3_span.stack, list) + assert len(urllib3_span.stack) > 1 + + assert "X-Capture-This-Too" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in urllib3_span.data["http"]["header"] + assert urllib3_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers + + def test_extract_custom_headers_exception( + self, span: "InstanaSpan", caplog: "LogCaptureFixture", monkeypatch + ) -> None: + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + + request_headers = { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + } + + monkeypatch.setattr(span, "set_attribute", Exception("mocked error")) + caplog.set_level(logging.DEBUG, logger="instana") + extract_custom_headers(span, request_headers) + assert "urllib3 _extract_custom_headers error: " in caplog.messages + + def test_collect_response_exception( + self, span: "InstanaSpan", caplog: "LogCaptureFixture", monkeypatch + ) -> None: + monkeypatch.setattr(span, "set_attribute", Exception("mocked error")) + + caplog.set_level(logging.DEBUG, logger="instana") + collect_response(span, {}) + assert "urllib3 collect_response error: " in caplog.messages + + def test_collect_kvs_exception( + self, span: "InstanaSpan", caplog: "LogCaptureFixture", monkeypatch + ) -> None: + monkeypatch.setattr(span, "set_attribute", Exception("mocked error")) + + caplog.set_level(logging.DEBUG, logger="instana") + collect_kvs({}, (), {}) + assert "urllib3 _collect_kvs error: " in caplog.messages From c8930841ccd8e19c5a62d1b513421878865b6026 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 5 Aug 2024 14:10:22 +0200 Subject: [PATCH 084/172] chore: Advanced exclusion from coverage.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure coverage to advanced exclude some code we know won’t be executed, like "if TYPE_CHECKING" and "except ImportError". Signed-off-by: Paulo Vital --- .coveragerc | 5 +++++ pyproject.toml | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..88037559 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +exclude_lines = + pragma: no cover + if TYPE_CHECKING: + except ImportError: diff --git a/pyproject.toml b/pyproject.toml index a72eab13..247b90e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,8 @@ string = "instana:load" [project.optional-dependencies] dev = [ "pytest", + "pytest-cov", + "pytest-mock", ] [project.urls] @@ -77,3 +79,10 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/instana"] + +[tool.coverage.report] +exclude_also = [ + "pragma: no cover", + "if TYPE_CHECKING:", + "except ImportError:", + ] From 5bf2cd522294dde8a14a772dfc618d6021d82416 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 1 Aug 2024 13:12:59 -0700 Subject: [PATCH 085/172] test(OTel): enable instrumentation tests. Signed-off-by: Paulo Vital --- src/instana/__init__.py | 195 +++++++++++++++++++++++----------------- tests/conftest.py | 32 ++++++- 2 files changed, 143 insertions(+), 84 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index e6ae417b..3db1ca49 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -1,38 +1,36 @@ # coding=utf-8 +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2016 """ -▀████▀███▄ ▀███▀▄█▀▀▀█▄███▀▀██▀▀███ ██ ▀███▄ ▀███▀ ██ - ██ ███▄ █ ▄██ ▀█▀ ██ ▀█ ▄██▄ ███▄ █ ▄██▄ - ██ █ ███ █ ▀███▄ ██ ▄█▀██▄ █ ███ █ ▄█▀██▄ - ██ █ ▀██▄ █ ▀█████▄ ██ ▄█ ▀██ █ ▀██▄ █ ▄█ ▀██ - ██ █ ▀██▄█ ▄ ▀██ ██ ████████ █ ▀██▄█ ████████ - ██ █ ███ ██ ██ ██ █▀ ██ █ ███ █▀ ██ -▄████▄███▄ ██ █▀█████▀ ▄████▄ ▄███▄ ▄████▄███▄ ██ ▄███▄ ▄████▄ +Instana -https://www.instana.com/ +https://www.ibm.com/products/instana -Documentation: https://www.instana.com/docs/ +Documentation: https://www.ibm.com/docs/en/instana-observability/current Source Code: https://github.com/instana/python-sensor """ - +import importlib import os import sys -import importlib -from .version import VERSION -from instana.collector.helpers.runtime import is_autowrapt_instrumented, is_webhook_instrumented - -__author__ = 'Instana Inc.' -__copyright__ = 'Copyright 2020 Instana Inc.' -__credits__ = ['Pavlo Baron', 'Peter Giacomo Lombardo', 'Andrey Slotin'] -__license__ = 'MIT' -__maintainer__ = 'Peter Giacomo Lombardo' -__email__ = 'peter.lombardo@instana.com' +from instana.collector.helpers.runtime import ( + is_autowrapt_instrumented, + is_webhook_instrumented, +) +from instana.version import VERSION + +__author__ = "Instana Inc." +__copyright__ = "Copyright 2020 Instana Inc." +__credits__ = ["Pavlo Baron", "Peter Giacomo Lombardo", "Andrey Slotin"] +__license__ = "MIT" +__maintainer__ = "Peter Giacomo Lombardo" +__email__ = "peter.lombardo@instana.com" __version__ = VERSION # User configurable EUM API key for instana.helpers.eum_snippet() # pylint: disable=invalid-name -eum_api_key = '' +eum_api_key = "" # This Python package can be loaded into Python processes one of three ways: # 1. manual import statement @@ -42,8 +40,19 @@ # With such magic, we may get pulled into Python processes that we have no interest being in. # As a safety measure, we maintain a "do not load list" and if this process matches something # in that list, then we go sit in a corner quietly and don't load anything at all. -do_not_load_list = ["pip", "pip2", "pip3", "pipenv", "docker-compose", "easy_install", "easy_install-2.7", - "smtpd.py", "twine", "ufw", "unattended-upgrade"] +do_not_load_list = [ + "pip", + "pip2", + "pip3", + "pipenv", + "docker-compose", + "easy_install", + "easy_install-2.7", + "smtpd.py", + "twine", + "ufw", + "unattended-upgrade", +] def load(_): @@ -53,25 +62,38 @@ def load(_): """ # Work around https://bugs.python.org/issue32573 if not hasattr(sys, "argv"): - sys.argv = [''] + sys.argv = [""] return None + def apply_gevent_monkey_patch(): from gevent import monkey if os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS"): + def short_key(k): - return k[3:] if k.startswith('no-') else k - + return k[3:] if k.startswith("no-") else k + def key_to_bool(k): - return not k.startswith('no-') + return not k.startswith("no-") import inspect - all_accepted_patch_all_args = inspect.getfullargspec(monkey.patch_all)[0] - provided_options = os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS").replace(" ","").replace("--","").split(',') - provided_options = [k for k in provided_options if short_key(k) in all_accepted_patch_all_args] - fargs = {short_key(k): key_to_bool(k) for (k,v) in zip(provided_options, [True]*len(provided_options))} + all_accepted_patch_all_args = inspect.getfullargspec(monkey.patch_all)[0] + provided_options = ( + os.environ.get("INSTANA_GEVENT_MONKEY_OPTIONS") + .replace(" ", "") + .replace("--", "") + .split(",") + ) + provided_options = [ + k for k in provided_options if short_key(k) in all_accepted_patch_all_args + ] + + fargs = { + short_key(k): key_to_bool(k) + for (k, v) in zip(provided_options, [True] * len(provided_options)) + } monkey.patch_all(**fargs) else: monkey.patch_all() @@ -115,81 +137,92 @@ def lambda_handler(event, context): # Import the module specified in module_name handler_module = importlib.import_module(module_name) except ImportError: - print("Couldn't determine and locate default module handler: %s.%s" % (module_name, function_name)) + print( + f"Couldn't determine and locate default module handler: {module_name}.{function_name}" + ) else: # Now get the function and execute it if hasattr(handler_module, function_name): handler_function = getattr(handler_module, function_name) return handler_function(event, context) else: - print("Couldn't determine and locate default function handler: %s.%s" % (module_name, function_name)) + print( + f"Couldn't determine and locate default function handler: {module_name}.{function_name}" + ) def boot_agent(): """Initialize the Instana agent and conditionally load auto-instrumentation.""" - # Disable all the unused-import violations in this function - # pylint: disable=unused-import - # pylint: disable=import-outside-toplevel - import instana.singletons + import instana.singletons # noqa: F401 # Instrumentation if "INSTANA_DISABLE_AUTO_INSTR" not in os.environ: - # Import & initialize instrumentation - from .instrumentation.aws import lambda_inst - - from .instrumentation import sanic_inst - - from .instrumentation import fastapi_inst - from .instrumentation import starlette_inst - - from .instrumentation import asyncio - from .instrumentation.aiohttp import client - from .instrumentation.aiohttp import server - from .instrumentation import boto3_inst - + # TODO: remove the following entries as the migration of the + # instrumentation codes are finalised. - from .instrumentation import mysqlclient - - from .instrumentation.google.cloud import storage - from .instrumentation.google.cloud import pubsub - - from .instrumentation.celery import hooks - - from .instrumentation import cassandra_inst - from .instrumentation import couchbase_inst - from .instrumentation import flask - from .instrumentation import gevent_inst - from .instrumentation import grpcio - from .instrumentation.tornado import client - from .instrumentation.tornado import server - from .instrumentation import logging - from .instrumentation import pika - from .instrumentation import pymysql - from .instrumentation import psycopg2 - from .instrumentation import redis - from .instrumentation import sqlalchemy - from .instrumentation import urllib3 - from .instrumentation.django import middleware - from .instrumentation import pymongo + # Import & initialize instrumentation + from instana.instrumentation import ( + # asyncio, # noqa: F401 + # boto3_inst, # noqa: F401 + # cassandra_inst, # noqa: F401 + # couchbase_inst, # noqa: F401 + # fastapi_inst, # noqa: F401 + flask, # noqa: F401 + # gevent_inst, # noqa: F401 + # grpcio, # noqa: F401 + logging, # noqa: F401 + # mysqlclient, # noqa: F401 + # pika, # noqa: F401 + # psycopg2, # noqa: F401 + # pymongo, # noqa: F401 + # pymysql, # noqa: F401 + # redis, # noqa: F401 + # sqlalchemy, # noqa: F401 + # starlette_inst, # noqa: F401 + # sanic_inst, # noqa: F401 + urllib3, # noqa: F401 + ) + # from instana.instrumentation.aiohttp import ( + # client, # noqa: F401 + # server, # noqa: F401 + # ) + # from instana.instrumentation.aws import lambda_inst # noqa: F401 + # from instana.instrumentation.celery import hooks # noqa: F401 + # from instana.instrumentation.django import middleware # noqa: F401 + # from instana.instrumentation.google.cloud import ( + # pubsub, # noqa: F401 + # storage, # noqa: F401 + # ) + # from instana.instrumentation.tornado import ( + # client, # noqa: F401 + # server, # noqa: F401 + # ) # Hooks - from .hooks import hook_uwsgi + # from instana.hooks import hook_uwsgi # noqa: F401 -if 'INSTANA_DISABLE' not in os.environ: +if "INSTANA_DISABLE" not in os.environ: # There are cases when sys.argv may not be defined at load time. Seems to happen in embedded Python, # and some Pipenv installs. If this is the case, it's best effort. - if hasattr(sys, 'argv') and len(sys.argv) > 0 and (os.path.basename(sys.argv[0]) in do_not_load_list): + if ( + hasattr(sys, "argv") + and len(sys.argv) > 0 + and (os.path.basename(sys.argv[0]) in do_not_load_list) + ): if "INSTANA_DEBUG" in os.environ: - print("Instana: No use in monitoring this process type (%s). " - "Will go sit in a corner quietly." % os.path.basename(sys.argv[0])) + print( + f"Instana: No use in monitoring this process type ({os.path.basename(sys.argv[0])}). Will go sit in a corner quietly." + ) else: # Automatic gevent monkey patching # unless auto instrumentation is off, then the customer should do manual gevent monkey patching - if ((is_autowrapt_instrumented() or is_webhook_instrumented()) and - "INSTANA_DISABLE_AUTO_INSTR" not in os.environ and - importlib.util.find_spec("gevent")): + if ( + (is_autowrapt_instrumented() or is_webhook_instrumented()) + and "INSTANA_DISABLE_AUTO_INSTR" not in os.environ + and importlib.util.find_spec("gevent") + ): apply_gevent_monkey_patch() # AutoProfile if "INSTANA_AUTOPROFILE" in os.environ: diff --git a/tests/conftest.py b/tests/conftest.py index 0a4c88ce..9afda3cc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,6 @@ # Set our testing flags os.environ["INSTANA_TEST"] = "true" -os.environ["INSTANA_DISABLE_AUTO_INSTR"] = "true" # TODO: remove all "noqa: E402" from instana package imports and move the # block of env variables setting to below the imports after finishing the @@ -26,15 +25,42 @@ from instana.span_context import SpanContext # noqa: E402 from instana.tracer import InstanaTracerProvider # noqa: E402 +# Ignoring tests during OpenTelemetry migration. collect_ignore_glob = [ "*autoprofile*", - "*clients*", - "*frameworks*", + # "*clients*", + # "*frameworks*", "*platforms*", "*propagators*", "*w3c_trace_context*", ] +# TODO: remove the following entries as the migration of the instrumentation +# codes are finalised. +collect_ignore_glob.append("*clients/boto*") +collect_ignore_glob.append("*clients/test_cassandra*") +collect_ignore_glob.append("*clients/test_counchbase*") +collect_ignore_glob.append("*clients/test_google*") +collect_ignore_glob.append("*clients/test_mysql*") +collect_ignore_glob.append("*clients/test_pika*") +collect_ignore_glob.append("*clients/test_psycopg*") +collect_ignore_glob.append("*clients/test_pym*") +collect_ignore_glob.append("*clients/test_redis*") +collect_ignore_glob.append("*clients/test_sql*") + +collect_ignore_glob.append("*frameworks/test_aiohttp*") +collect_ignore_glob.append("*frameworks/test_asyncio*") +collect_ignore_glob.append("*frameworks/test_celery*") +collect_ignore_glob.append("*frameworks/test_django*") +collect_ignore_glob.append("*frameworks/test_fastapi*") +collect_ignore_glob.append("*frameworks/test_gevent*") +collect_ignore_glob.append("*frameworks/test_grpcio*") +collect_ignore_glob.append("*frameworks/test_pyramid*") +collect_ignore_glob.append("*frameworks/test_sanic*") +collect_ignore_glob.append("*frameworks/test_starlette*") +collect_ignore_glob.append("*frameworks/test_tornado*") +collect_ignore_glob.append("*frameworks/test_wsgi*") + # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will # be run explicitly. (So always exclude them here) if not os.environ.get("CASSANDRA_TEST"): From 0fa0d03c86c357aa536705eaafeb785e61a19041 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 19 Aug 2024 16:37:09 +0530 Subject: [PATCH 086/172] fix: context propagation in nested spans Signed-off-by: Varsha GS --- src/instana/tracer.py | 4 ++-- tests/test_tracer.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/instana/tracer.py b/src/instana/tracer.py index f7ffdcfc..fc5301f9 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -30,7 +30,7 @@ from instana.recorder import StanRecorder from instana.sampling import InstanaSampler, Sampler from instana.span.kind import EXIT_SPANS -from instana.span.span import InstanaSpan +from instana.span.span import InstanaSpan, get_current_span from instana.span_context import SpanContext from instana.util.ids import generate_id @@ -118,7 +118,7 @@ def start_span( record_exception: bool = True, set_status_on_exception: bool = True, ) -> InstanaSpan: - parent_context = span_context + parent_context = span_context if span_context else get_current_span().get_span_context() if parent_context is not None and not isinstance(parent_context, SpanContext): raise TypeError("parent_context must be an Instana SpanContext or None.") diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 62f3bc15..16c2042f 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,15 +1,15 @@ # (c) Copyright IBM Corp. 2024 -from opentelemetry.trace import set_span_in_context -from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID import pytest + +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE + from instana.agent.test import TestAgent from instana.recorder import StanRecorder from instana.sampling import InstanaSampler -from instana.span.span import InstanaSpan +from instana.span.span import InstanaSpan, get_current_span, INVALID_SPAN_ID, INVALID_SPAN from instana.span_context import SpanContext from instana.tracer import InstanaTracer, InstanaTracerProvider -from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: @@ -99,6 +99,28 @@ def test_tracer_start_as_current_span(tracer_provider: InstanaTracerProvider) -> assert span.name == span_name +def test_tracer_nested_span(tracer_provider: InstanaTracerProvider) -> None: + tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + parent_span_name = "parent-span" + child_span_name = "child-span" + with tracer.start_as_current_span(name=parent_span_name) as pspan: + assert get_current_span() is pspan + with tracer.start_as_current_span(name=child_span_name) as cspan: + assert get_current_span() is cspan + assert cspan.parent_id == pspan.context.span_id + # child span goes out of scope + assert cspan.end_time is not None + assert get_current_span() is pspan + # parent span goes out of scope + assert pspan.end_time is not None + assert get_current_span() is INVALID_SPAN + + def test_tracer_create_span_context( span_context: SpanContext, tracer_provider: InstanaTracerProvider ) -> None: From 091090e4bebd06f68ea0a95b3cd1bbe4e3c93891 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 12 Aug 2024 19:04:46 +0530 Subject: [PATCH 087/172] refactor(instrumentation): wsgi Signed-off-by: Varsha GS --- src/instana/instrumentation/wsgi.py | 70 ++++++++++++++++++----------- src/instana/middleware.py | 2 +- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 3a981d2f..a6e24147 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -4,15 +4,17 @@ """ Instana WSGI Middleware """ -import opentracing as ot -import opentracing.ext.tags as tags -from ..singletons import agent, tracer -from ..util.secrets import strip_secrets_from_query +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry import context, trace + +from instana.propagators.format import Format +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query class InstanaWSGIMiddleware(object): - """ Instana WSGI middleware """ + """Instana WSGI middleware""" def __init__(self, app): self.app = app @@ -22,38 +24,52 @@ def __call__(self, environ, start_response): def new_start_response(status, headers, exc_info=None): """Modified start response with additional headers.""" - tracer.inject(self.scope.span.context, ot.Format.HTTP_HEADERS, headers) - headers.append(('Server-Timing', "intid;desc=%s" % self.scope.span.context.trace_id)) + tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) + headers.append( + ("Server-Timing", "intid;desc=%s" % self.span.context.trace_id) + ) - res = start_response(status, headers, exc_info) + headers_str = [(header[0], str(header[1])) if not isinstance(header[1], str) else header for header in headers] + res = start_response(status, headers_str, exc_info) - sc = status.split(' ')[0] + sc = status.split(" ")[0] if 500 <= int(sc): - self.scope.span.mark_as_errored() + self.span.mark_as_errored() - self.scope.span.set_tag(tags.HTTP_STATUS_CODE, sc) - self.scope.close() + self.span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, sc) + if self.span and self.span.is_recording(): + self.span.end() + if self.token: + context.detach(self.token) return res - ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) - self.scope = tracer.start_active_span("wsgi", child_of=ctx) + span_context = tracer.extract(Format.HTTP_HEADERS, env) + self.span = tracer.start_span("wsgi", span_context=span_context) + + ctx = trace.set_span_in_context(self.span) + self.token = context.attach(ctx) if agent.options.extra_http_headers is not None: for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS - wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_') + wsgi_header = ("HTTP_" + custom_header.upper()).replace("-", "_") if wsgi_header in env: - self.scope.span.set_tag("http.header.%s" % custom_header, env[wsgi_header]) - - if 'PATH_INFO' in env: - self.scope.span.set_tag('http.path', env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) - self.scope.span.set_tag("http.params", scrubbed_params) - if 'REQUEST_METHOD' in env: - self.scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD']) - if 'HTTP_HOST' in env: - self.scope.span.set_tag("http.host", env['HTTP_HOST']) + self.span.set_attribute( + "http.header.%s" % custom_header, env[wsgi_header] + ) + + if "PATH_INFO" in env: + self.span.set_attribute("http.path", env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + self.span.set_attribute("http.params", scrubbed_params) + if "REQUEST_METHOD" in env: + self.span.set_attribute(SpanAttributes.HTTP_METHOD, env["REQUEST_METHOD"]) + if "HTTP_HOST" in env: + self.span.set_attribute("http.host", env["HTTP_HOST"]) return self.app(environ, new_start_response) diff --git a/src/instana/middleware.py b/src/instana/middleware.py index f731931d..6fbc9295 100644 --- a/src/instana/middleware.py +++ b/src/instana/middleware.py @@ -3,4 +3,4 @@ from .instrumentation.wsgi import InstanaWSGIMiddleware -from .instrumentation.asgi import InstanaASGIMiddleware \ No newline at end of file +# from .instrumentation.asgi import InstanaASGIMiddleware From 307d8ea59856fe39ccc213648a6f547a0a7c15da Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 12 Aug 2024 19:08:37 +0530 Subject: [PATCH 088/172] wsgi: Add Bottle app Signed-off-by: Varsha GS --- tests/apps/bottle_app/__init__.py | 8 ++++++++ tests/apps/bottle_app/app.py | 32 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 tests/apps/bottle_app/__init__.py create mode 100644 tests/apps/bottle_app/app.py diff --git a/tests/apps/bottle_app/__init__.py b/tests/apps/bottle_app/__init__.py new file mode 100644 index 00000000..e45f9ee1 --- /dev/null +++ b/tests/apps/bottle_app/__init__.py @@ -0,0 +1,8 @@ +import os +from .app import bottle_server as server +from ..utils import launch_background_thread + +app_thread = None + +if not os.environ.get('CASSANDRA_TEST') and app_thread is None: + app_thread = launch_background_thread(server.serve_forever, "Bottle") \ No newline at end of file diff --git a/tests/apps/bottle_app/app.py b/tests/apps/bottle_app/app.py new file mode 100644 index 00000000..3f299837 --- /dev/null +++ b/tests/apps/bottle_app/app.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) Copyright IBM Corp. 2024 + +import logging + +from wsgiref.simple_server import make_server +from bottle import default_app + +from tests.helpers import testenv +from instana.middleware import InstanaWSGIMiddleware + +logging.basicConfig(level=logging.WARNING) +logger = logging.getLogger(__name__) + +testenv["wsgi_port"] = 10811 +testenv["wsgi_server"] = ("http://127.0.0.1:" + str(testenv["wsgi_port"])) + +app = default_app() + +@app.route("/") +def hello(): + return "

🐍 Hello Stan! 🦄

" + +# Wrap the application with the Instana WSGI Middleware +app = InstanaWSGIMiddleware(app) +bottle_server = make_server('127.0.0.1', testenv["wsgi_port"], app) + +if __name__ == "__main__": + bottle_server.request_queue_size = 20 + bottle_server.serve_forever() From e4329f7913fa023f12edef6c3d542aa4f3938edc Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 12 Aug 2024 19:09:27 +0530 Subject: [PATCH 089/172] wsgi: Adapt tests to middleware and bottle Signed-off-by: Varsha GS --- tests/frameworks/test_wsgi.py | 138 +++++++++++----------------------- 1 file changed, 42 insertions(+), 96 deletions(-) diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index 3c66b79b..33c17b0d 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -5,16 +5,17 @@ import urllib3 import unittest -import tests.apps.flask_app -from ..helpers import testenv +from tests.apps import bottle_app +from tests.helpers import testenv from instana.singletons import agent, tracer +from instana.span.span import get_current_span class TestWSGI(unittest.TestCase): def setUp(self): """ Clear all spans before a test run """ self.http = urllib3.PoolManager() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() time.sleep(0.1) @@ -27,17 +28,17 @@ def test_vanilla_requests(self): spans = self.recorder.queued_spans() self.assertEqual(1, len(spans)) - self.assertIsNone(tracer.active_span) + assert get_current_span().is_recording() is False self.assertEqual(response.status, 200) def test_get_request(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] @@ -48,11 +49,11 @@ def test_get_request(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -81,23 +82,24 @@ def test_get_request(self): # wsgi self.assertEqual("wsgi", wsgi_span.n) self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) + self.assertEqual('/', wsgi_span.data["http"]["path"]) self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) + self.assertEqual("200", wsgi_span.data["http"]["status"]) self.assertIsNone(wsgi_span.data["http"]["error"]) self.assertIsNone(wsgi_span.stack) + @unittest.skip("Suppression is not yet handled") def test_synthetic_request(self): headers = { 'X-INSTANA-SYNTHETIC': '1' } - with tracer.start_active_span('test'): + with tracer.start_as_current_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) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] @@ -107,66 +109,6 @@ def test_synthetic_request(self): 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') - - spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) - self.assertIsNone(tracer.active_span) - - spacedust_span = spans[0] - asteroid_span = spans[1] - wsgi_span = spans[2] - urllib3_span = spans[3] - test_span = spans[4] - - self.assertTrue(response) - self.assertEqual(200, response.status) - - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) - - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) - - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') - - self.assertIn('Server-Timing', response.headers) - server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) - - # Same traceId - trace_id = test_span.t - self.assertEqual(trace_id, urllib3_span.t) - self.assertEqual(trace_id, wsgi_span.t) - self.assertEqual(trace_id, asteroid_span.t) - self.assertEqual(trace_id, spacedust_span.t) - - # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) - self.assertEqual(asteroid_span.p, wsgi_span.s) - self.assertEqual(spacedust_span.p, asteroid_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) - self.assertIsNone(asteroid_span.ec) - self.assertIsNone(spacedust_span.ec) - - # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/complex', wsgi_span.data["http"]["url"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) def test_custom_header_capture(self): # Hack together a manual custom headers list @@ -176,13 +118,13 @@ def test_custom_header_capture(self): request_headers['X-Capture-This'] = 'this' request_headers['X-Capture-That'] = 'that' - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] @@ -193,11 +135,11 @@ def test_custom_header_capture(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -222,9 +164,9 @@ def test_custom_header_capture(self): # wsgi self.assertEqual("wsgi", wsgi_span.n) self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) + self.assertEqual('/', wsgi_span.data["http"]["path"]) self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) + self.assertEqual("200", wsgi_span.data["http"]["status"]) self.assertIsNone(wsgi_span.data["http"]["error"]) self.assertIsNone(wsgi_span.stack) @@ -234,13 +176,13 @@ def test_custom_header_capture(self): self.assertEqual("that", wsgi_span.data["http"]["header"]["X-Capture-That"]) def test_secret_scrubbing(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/?secret=shhh') spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] @@ -251,11 +193,11 @@ def test_secret_scrubbing(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -280,10 +222,10 @@ def test_secret_scrubbing(self): # wsgi self.assertEqual("wsgi", wsgi_span.n) self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["url"]) + self.assertEqual('/', wsgi_span.data["http"]["path"]) self.assertEqual('secret=', wsgi_span.data["http"]["params"]) self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual(200, wsgi_span.data["http"]["status"]) + self.assertEqual("200", wsgi_span.data["http"]["status"]) self.assertIsNone(wsgi_span.data["http"]["error"]) self.assertIsNone(wsgi_span.stack) @@ -302,16 +244,18 @@ def test_with_incoming_context(self): wsgi_span = spans[0] - self.assertEqual(wsgi_span.t, '0000000000000001') - self.assertEqual(wsgi_span.p, '0000000000000001') + # self.assertEqual(wsgi_span.t, '0000000000000001') + # self.assertEqual(wsgi_span.p, '0000000000000001') + assert wsgi_span.t == 1 + assert wsgi_span.p == 1 self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -335,16 +279,18 @@ def test_with_incoming_mixed_case_context(self): wsgi_span = spans[0] - self.assertEqual(wsgi_span.t, '0000000000000001') - self.assertEqual(wsgi_span.p, '0000000000000001') + # self.assertEqual(wsgi_span.t, '0000000000000001') + # self.assertEqual(wsgi_span.p, '0000000000000001') + assert wsgi_span.t == 1 + assert wsgi_span.p == 1 self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') @@ -354,13 +300,13 @@ def test_with_incoming_mixed_case_context(self): self.assertEqual(response.headers['Server-Timing'], server_timing_value) def test_response_headers(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) - self.assertIsNone(tracer.active_span) + assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] @@ -371,11 +317,11 @@ def test_response_headers(self): self.assertIn('X-INSTANA-T', response.headers) self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], wsgi_span.t) + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) self.assertIn('X-INSTANA-S', response.headers) self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], wsgi_span.s) + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) self.assertIn('X-INSTANA-L', response.headers) self.assertEqual(response.headers['X-INSTANA-L'], '1') From 4f1bce8978309892132b76f1760244e5086b88e5 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 12 Aug 2024 19:21:17 +0530 Subject: [PATCH 090/172] refactor(tests): use python standard assert stmts Signed-off-by: Varsha GS --- tests/frameworks/test_wsgi.py | 254 +++++++++++++++++----------------- 1 file changed, 127 insertions(+), 127 deletions(-) diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index 33c17b0d..c521e384 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -27,9 +27,9 @@ def test_vanilla_requests(self): response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) assert get_current_span().is_recording() is False - self.assertEqual(response.status, 200) + assert response.status == 200 def test_get_request(self): with tracer.start_as_current_span("test"): @@ -37,56 +37,56 @@ def test_get_request(self): spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('Server-Timing', response.headers) + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s - self.assertIsNone(wsgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert wsgi_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["path"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual("200", wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert '127.0.0.1:' + str(testenv['wsgi_port']) == wsgi_span.data["http"]["host"] + assert '/' == wsgi_span.data["http"]["path"] + assert 'GET' == wsgi_span.data["http"]["method"] + assert "200" == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None @unittest.skip("Suppression is not yet handled") def test_synthetic_request(self): @@ -98,16 +98,16 @@ def test_synthetic_request(self): spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) assert get_current_span().is_recording() is False 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) + assert wsgi_span.sy + assert urllib3_span.sy is None + assert test_span.sy is None def test_custom_header_capture(self): @@ -123,57 +123,57 @@ def test_custom_header_capture(self): spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('Server-Timing', response.headers) + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["path"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual("200", wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) - - self.assertIn("X-Capture-This", wsgi_span.data["http"]["header"]) - self.assertEqual("this", wsgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", wsgi_span.data["http"]["header"]) - self.assertEqual("that", wsgi_span.data["http"]["header"]["X-Capture-That"]) + assert "wsgi" == wsgi_span.n + assert '127.0.0.1:' + str(testenv['wsgi_port']) == wsgi_span.data["http"]["host"] + assert '/' == wsgi_span.data["http"]["path"] + assert 'GET' == wsgi_span.data["http"]["method"] + assert "200" == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None + + assert "X-Capture-This" in wsgi_span.data["http"]["header"] + assert "this" == wsgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in wsgi_span.data["http"]["header"] + assert "that" == wsgi_span.data["http"]["header"]["X-Capture-That"] def test_secret_scrubbing(self): with tracer.start_as_current_span("test"): @@ -181,53 +181,53 @@ def test_secret_scrubbing(self): spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('Server-Timing', response.headers) + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, wsgi_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(wsgi_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(wsgi_span.ec) + assert test_span.ec is None + assert urllib3_span.ec is None + assert wsgi_span.ec is None # wsgi - self.assertEqual("wsgi", wsgi_span.n) - self.assertEqual('127.0.0.1:' + str(testenv['wsgi_port']), wsgi_span.data["http"]["host"]) - self.assertEqual('/', wsgi_span.data["http"]["path"]) - self.assertEqual('secret=', wsgi_span.data["http"]["params"]) - self.assertEqual('GET', wsgi_span.data["http"]["method"]) - self.assertEqual("200", wsgi_span.data["http"]["status"]) - self.assertIsNone(wsgi_span.data["http"]["error"]) - self.assertIsNone(wsgi_span.stack) + assert "wsgi" == wsgi_span.n + assert '127.0.0.1:' + str(testenv['wsgi_port']) == wsgi_span.data["http"]["host"] + assert '/' == wsgi_span.data["http"]["path"] + assert 'secret=' == wsgi_span.data["http"]["params"] + assert 'GET' == wsgi_span.data["http"]["method"] + assert "200" == wsgi_span.data["http"]["status"] + assert wsgi_span.data["http"]["error"] is None + assert wsgi_span.stack is None def test_with_incoming_context(self): request_headers = dict() @@ -236,33 +236,33 @@ def test_with_incoming_context(self): response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) wsgi_span = spans[0] - # self.assertEqual(wsgi_span.t, '0000000000000001') - # self.assertEqual(wsgi_span.p, '0000000000000001') + # assert wsgi_span.t == '0000000000000001' + # assert wsgi_span.p == '0000000000000001' assert wsgi_span.t == 1 assert wsgi_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('Server-Timing', response.headers) + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value def test_with_incoming_mixed_case_context(self): request_headers = dict() @@ -271,33 +271,33 @@ def test_with_incoming_mixed_case_context(self): response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) wsgi_span = spans[0] - # self.assertEqual(wsgi_span.t, '0000000000000001') - # self.assertEqual(wsgi_span.p, '0000000000000001') + # assert wsgi_span.t == '0000000000000001' + # assert wsgi_span.p == '0000000000000001' assert wsgi_span.t == 1 assert wsgi_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('Server-Timing', response.headers) + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value def test_response_headers(self): with tracer.start_as_current_span("test"): @@ -305,27 +305,27 @@ def test_response_headers(self): spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) assert get_current_span().is_recording() is False wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('Server-Timing', response.headers) + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % wsgi_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers['Server-Timing'] == server_timing_value From 7180c31e7b1a6ec1c16989a7465a9442666c3c12 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 16 Aug 2024 12:43:47 +0530 Subject: [PATCH 091/172] style: Add TypeHints, fix imports Signed-off-by: Varsha GS --- src/instana/instrumentation/wsgi.py | 7 ++++--- src/instana/middleware.py | 2 +- tests/apps/bottle_app/__init__.py | 4 ++-- tests/frameworks/test_wsgi.py | 30 ++++++++++++++--------------- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index a6e24147..40d7e340 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -4,6 +4,7 @@ """ Instana WSGI Middleware """ +from typing import Dict, Any, Callable, List, Tuple, Optional from opentelemetry.semconv.trace import SpanAttributes from opentelemetry import context, trace @@ -16,13 +17,13 @@ class InstanaWSGIMiddleware(object): """Instana WSGI middleware""" - def __init__(self, app): + def __init__(self, app: object) -> None: self.app = app - def __call__(self, environ, start_response): + def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: env = environ - def new_start_response(status, headers, exc_info=None): + def new_start_response(status: str, headers: List[Tuple[object, ...]], exc_info: Optional[Exception] = None) -> object: """Modified start response with additional headers.""" tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) headers.append( diff --git a/src/instana/middleware.py b/src/instana/middleware.py index 6fbc9295..71fa0efa 100644 --- a/src/instana/middleware.py +++ b/src/instana/middleware.py @@ -2,5 +2,5 @@ # (c) Copyright Instana Inc. 2017 -from .instrumentation.wsgi import InstanaWSGIMiddleware +from instana.instrumentation.wsgi import InstanaWSGIMiddleware # from .instrumentation.asgi import InstanaASGIMiddleware diff --git a/tests/apps/bottle_app/__init__.py b/tests/apps/bottle_app/__init__.py index e45f9ee1..0aa902b2 100644 --- a/tests/apps/bottle_app/__init__.py +++ b/tests/apps/bottle_app/__init__.py @@ -1,6 +1,6 @@ import os -from .app import bottle_server as server -from ..utils import launch_background_thread +from tests.apps.bottle_app.app import bottle_server as server +from tests.apps.utils import launch_background_thread app_thread = None diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index c521e384..df2c9fcc 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -3,7 +3,8 @@ import time import urllib3 -import unittest +import pytest +from typing import Generator from tests.apps import bottle_app from tests.helpers import testenv @@ -11,19 +12,16 @@ from instana.span.span import get_current_span -class TestWSGI(unittest.TestCase): - def setUp(self): +class TestWSGI: + @pytest.fixture(autouse=True) + def _setUp(self) -> Generator[None, None, None]: """ Clear all spans before a test run """ self.http = urllib3.PoolManager() self.recorder = tracer.span_processor self.recorder.clear_spans() time.sleep(0.1) - def tearDown(self): - """ Do nothing for now """ - return None - - def test_vanilla_requests(self): + def test_vanilla_requests(self) -> None: response = self.http.request('GET', testenv["wsgi_server"] + '/') spans = self.recorder.queued_spans() @@ -31,7 +29,7 @@ def test_vanilla_requests(self): assert get_current_span().is_recording() is False assert response.status == 200 - def test_get_request(self): + def test_get_request(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/') @@ -88,8 +86,8 @@ def test_get_request(self): assert wsgi_span.data["http"]["error"] is None assert wsgi_span.stack is None - @unittest.skip("Suppression is not yet handled") - def test_synthetic_request(self): + @pytest.mark.skip("Suppression is not yet handled") + def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' } @@ -110,7 +108,7 @@ def test_synthetic_request(self): assert test_span.sy is None - def test_custom_header_capture(self): + def test_custom_header_capture(self) -> None: # Hack together a manual custom headers list agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] @@ -175,7 +173,7 @@ def test_custom_header_capture(self): assert "X-Capture-That" in wsgi_span.data["http"]["header"] assert "that" == wsgi_span.data["http"]["header"]["X-Capture-That"] - def test_secret_scrubbing(self): + def test_secret_scrubbing(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/?secret=shhh') @@ -229,7 +227,7 @@ def test_secret_scrubbing(self): assert wsgi_span.data["http"]["error"] is None assert wsgi_span.stack is None - def test_with_incoming_context(self): + def test_with_incoming_context(self) -> None: request_headers = dict() request_headers['X-INSTANA-T'] = '0000000000000001' request_headers['X-INSTANA-S'] = '0000000000000001' @@ -264,7 +262,7 @@ def test_with_incoming_context(self): server_timing_value = "intid;desc=%s" % wsgi_span.t assert response.headers['Server-Timing'] == server_timing_value - def test_with_incoming_mixed_case_context(self): + def test_with_incoming_mixed_case_context(self) -> None: request_headers = dict() request_headers['X-InSTANa-T'] = '0000000000000001' request_headers['X-instana-S'] = '0000000000000001' @@ -299,7 +297,7 @@ def test_with_incoming_mixed_case_context(self): server_timing_value = "intid;desc=%s" % wsgi_span.t assert response.headers['Server-Timing'] == server_timing_value - def test_response_headers(self): + def test_response_headers(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/') From b9c05d12507745a47873918b72b8869fa071653a Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 16 Aug 2024 14:44:22 +0530 Subject: [PATCH 092/172] wsgi: enable tests after refactor Signed-off-by: Varsha GS --- tests/conftest.py | 1 - tests/requirements-310.txt | 1 + tests/requirements-312.txt | 1 + tests/requirements-313.txt | 1 + tests/requirements.txt | 1 + 5 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9afda3cc..c47d61d7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,7 +59,6 @@ collect_ignore_glob.append("*frameworks/test_sanic*") collect_ignore_glob.append("*frameworks/test_starlette*") collect_ignore_glob.append("*frameworks/test_tornado*") -collect_ignore_glob.append("*frameworks/test_wsgi*") # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will # be run explicitly. (So always exclude them here) diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 61bcb26a..83242aa0 100644 --- a/tests/requirements-310.txt +++ b/tests/requirements-310.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=5.0 diff --git a/tests/requirements-312.txt b/tests/requirements-312.txt index 77015129..fa87e2ee 100644 --- a/tests/requirements-312.txt +++ b/tests/requirements-312.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=5.0a1 --pre diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index b0f9588a..2f927181 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=5.0a1 --pre diff --git a/tests/requirements.txt b/tests/requirements.txt index d1f4a5d9..026f94f9 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,6 +1,7 @@ aiofiles>=0.5.0 aiohttp>=3.8.3 boto3>=1.17.74 +bottle>=0.12.25 celery>=5.2.7 coverage>=5.5 Django>=4.2.4 From e32ada1eef68c0b0a343fbcad376082c539d27fb Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 16 Aug 2024 14:56:45 +0530 Subject: [PATCH 093/172] refactor(tests): modify port names for flask and wsgi tests Signed-off-by: Varsha GS --- tests/apps/bottle_app/app.py | 2 +- tests/apps/flask_app/app.py | 35 +-------- tests/clients/boto3/test_boto3_sqs.py | 2 +- tests/clients/test_urllib3.py | 98 ++++++++++++------------- tests/frameworks/test_aiohttp_client.py | 32 ++++---- tests/frameworks/test_asyncio.py | 8 +- tests/frameworks/test_flask.py | 92 +++++++++++------------ tests/frameworks/test_gevent.py | 4 +- tests/frameworks/test_wsgi.py | 6 +- 9 files changed, 125 insertions(+), 154 deletions(-) diff --git a/tests/apps/bottle_app/app.py b/tests/apps/bottle_app/app.py index 3f299837..cd56c138 100644 --- a/tests/apps/bottle_app/app.py +++ b/tests/apps/bottle_app/app.py @@ -14,7 +14,7 @@ logging.basicConfig(level=logging.WARNING) logger = logging.getLogger(__name__) -testenv["wsgi_port"] = 10811 +testenv["wsgi_port"] = 10812 testenv["wsgi_server"] = ("http://127.0.0.1:" + str(testenv["wsgi_port"])) app = default_app() diff --git a/tests/apps/flask_app/app.py b/tests/apps/flask_app/app.py index 104fbe59..e49f8fa1 100755 --- a/tests/apps/flask_app/app.py +++ b/tests/apps/flask_app/app.py @@ -22,19 +22,18 @@ pass from tests.helpers import testenv -from instana.singletons import tracer logging.basicConfig(level=logging.WARNING) logger = logging.getLogger(__name__) -testenv["wsgi_port"] = 10811 -testenv["wsgi_server"] = ("http://127.0.0.1:" + str(testenv["wsgi_port"])) +testenv["flask_port"] = 10811 +testenv["flask_server"] = ("http://127.0.0.1:" + str(testenv["flask_port"])) app = Flask(__name__) app.debug = False app.use_reloader = False -flask_server = make_server('127.0.0.1', testenv["wsgi_port"], app.wsgi_app) +flask_server = make_server('127.0.0.1', testenv["flask_port"], app.wsgi_app) class InvalidUsage(Exception): @@ -79,34 +78,6 @@ def username_hello(username): return u"

🐍 Hello %s! 🦄

" % username -@app.route("/complex") -def gen_opentelemetry(): - with tracer.start_as_current_span("asteroid") as pspan: - pspan.set_attribute(SpanAttributes.COMPONENT, "Python simple example app") - pspan.set_attribute( - SpanAttributes.SPAN_KIND, SpanAttributes.SPAN_KIND_RPC_SERVER - ) - pspan.set_attribute(SpanAttributes.PEER_HOSTNAME, "localhost") - pspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/one") - pspan.set_attribute(SpanAttributes.HTTP_METHOD, "GET") - pspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 200) - pspan.add_event(name="gen_opentelemetry", attributes={"foo": "bar"}) - - span_context = pspan.get_span_context() - - with tracer.start_active_span("spacedust", span_context=span_context) as cspan: - cspan.set_attribute( - SpanAttributes.SPAN_KIND, SpanAttributes.SPAN_KIND_RPC_CLIENT - ) - cspan.set_attribute(SpanAttributes.PEER_HOSTNAME, "localhost") - cspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/two") - cspan.set_attribute(SpanAttributes.HTTP_METHOD, "POST") - cspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 204) - cspan.set_baggage_item("someBaggage", "someValue") - - return "

🐍 Generated some OT spans... 🦄

" - - @app.route("/301") def threehundredone(): return redirect('/', code=301) diff --git a/tests/clients/boto3/test_boto3_sqs.py b/tests/clients/boto3/test_boto3_sqs.py index a673f7b9..fc9eb57d 100644 --- a/tests/clients/boto3/test_boto3_sqs.py +++ b/tests/clients/boto3/test_boto3_sqs.py @@ -154,7 +154,7 @@ def test_send_message_as_root_exit_span(self): def test_app_boto3_sqs(self): with tracer.start_active_span('test'): - self.http_client.request('GET', testenv["wsgi_server"] + '/boto3/sqs') + self.http_client.request('GET', testenv["flask_server"] + '/boto3/sqs') spans = self.recorder.queued_spans() self.assertEqual(5, len(spans)) diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index 1347d0a1..5c735614 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -40,7 +40,7 @@ def _setup(self) -> Generator[None, None, None]: agent.options.allow_exit_as_root = False def test_vanilla_requests(self) -> None: - r = self.http.request("GET", testenv["wsgi_server"] + "/") + r = self.http.request("GET", testenv["flask_server"] + "/") assert r.status == 200 spans = self.recorder.queued_spans() @@ -51,7 +51,7 @@ def test_parallel_requests(self) -> None: def task(num): r = http_pool_5.request( - "GET", testenv["wsgi_server"] + "/", fields={"num": num} + "GET", testenv["flask_server"] + "/", fields={"num": num} ) return r @@ -72,7 +72,7 @@ def task(num): def test_customers_setup_zd_26466(self) -> None: def make_request(u=None) -> int: sleep(10) - x = requests.get(testenv["wsgi_server"] + "/") + x = requests.get(testenv["flask_server"] + "/") sleep(10) return x.status_code @@ -90,7 +90,7 @@ def make_request(u=None) -> int: def test_get_request(self): with tracer.start_as_current_span("test"): - r = self.http.request("GET", testenv["wsgi_server"] + "/") + r = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -118,7 +118,7 @@ def test_get_request(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/" assert wsgi_span.data["http"]["method"] == "GET" @@ -130,7 +130,7 @@ def test_get_request(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -171,7 +171,7 @@ def test_get_request_https(self): def test_get_request_as_root_exit_span(self): agent.options.allow_exit_as_root = True - r = self.http.request("GET", testenv["wsgi_server"] + "/") + r = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() assert len(spans) == 2 @@ -197,7 +197,7 @@ def test_get_request_as_root_exit_span(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/" assert wsgi_span.data["http"]["method"] == "GET" @@ -208,7 +208,7 @@ def test_get_request_as_root_exit_span(self): # urllib3 assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -216,7 +216,7 @@ def test_get_request_as_root_exit_span(self): def test_get_request_with_query(self): with tracer.start_as_current_span("test"): - r = self.http.request("GET", testenv["wsgi_server"] + "/?one=1&two=2") + r = self.http.request("GET", testenv["flask_server"] + "/?one=1&two=2") spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -245,7 +245,7 @@ def test_get_request_with_query(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/" assert wsgi_span.data["http"]["method"] == "GET" @@ -257,7 +257,7 @@ def test_get_request_with_query(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack @@ -267,7 +267,7 @@ def test_get_request_with_query(self): def test_get_request_with_alt_query(self): with tracer.start_as_current_span("test"): r = self.http.request( - "GET", testenv["wsgi_server"] + "/", fields={"one": "1", "two": 2} + "GET", testenv["flask_server"] + "/", fields={"one": "1", "two": 2} ) spans = self.recorder.queued_spans() @@ -297,7 +297,7 @@ def test_get_request_with_alt_query(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/" assert wsgi_span.data["http"]["method"] == "GET" @@ -309,7 +309,7 @@ def test_get_request_with_alt_query(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span.data["http"]["params"] in ["one=1&two=2", "two=2&one=1"] assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack @@ -318,7 +318,7 @@ def test_get_request_with_alt_query(self): def test_put_request(self): with tracer.start_as_current_span("test"): - r = self.http.request("PUT", testenv["wsgi_server"] + "/notfound") + r = self.http.request("PUT", testenv["flask_server"] + "/notfound") spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -347,7 +347,7 @@ def test_put_request(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/notfound" assert wsgi_span.data["http"]["method"] == "PUT" @@ -359,7 +359,7 @@ def test_put_request(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 404 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/notfound" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/notfound" assert urllib3_span.data["http"]["method"] == "PUT" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -367,7 +367,7 @@ def test_put_request(self): def test_301_redirect(self): with tracer.start_as_current_span("test"): - r = self.http.request("GET", testenv["wsgi_server"] + "/301") + r = self.http.request("GET", testenv["flask_server"] + "/301") spans = self.recorder.queued_spans() assert len(spans) == 5 @@ -405,7 +405,7 @@ def test_301_redirect(self): # wsgi assert wsgi_span1.n == "wsgi" assert wsgi_span1.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span1.data["http"]["url"] == "/" assert wsgi_span1.data["http"]["method"] == "GET" @@ -415,7 +415,7 @@ def test_301_redirect(self): assert wsgi_span2.n == "wsgi" assert wsgi_span2.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span2.data["http"]["url"] == "/301" assert wsgi_span2.data["http"]["method"] == "GET" @@ -427,7 +427,7 @@ def test_301_redirect(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span1.n == "urllib3" assert urllib3_span1.data["http"]["status"] == 200 - assert urllib3_span1.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span1.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span1.data["http"]["method"] == "GET" assert urllib3_span1.stack assert isinstance(urllib3_span1.stack, list) @@ -435,7 +435,7 @@ def test_301_redirect(self): assert urllib3_span2.n == "urllib3" assert urllib3_span2.data["http"]["status"] == 301 - assert urllib3_span2.data["http"]["url"] == testenv["wsgi_server"] + "/301" + assert urllib3_span2.data["http"]["url"] == testenv["flask_server"] + "/301" assert urllib3_span2.data["http"]["method"] == "GET" assert urllib3_span2.stack assert isinstance(urllib3_span2.stack, list) @@ -443,7 +443,7 @@ def test_301_redirect(self): def test_302_redirect(self): with tracer.start_as_current_span("test"): - r = self.http.request("GET", testenv["wsgi_server"] + "/302") + r = self.http.request("GET", testenv["flask_server"] + "/302") spans = self.recorder.queued_spans() assert len(spans) == 5 @@ -481,7 +481,7 @@ def test_302_redirect(self): # wsgi assert wsgi_span1.n == "wsgi" assert wsgi_span1.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span1.data["http"]["url"] == "/" assert wsgi_span1.data["http"]["method"] == "GET" @@ -491,7 +491,7 @@ def test_302_redirect(self): assert wsgi_span2.n == "wsgi" assert wsgi_span2.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span2.data["http"]["url"] == "/302" assert wsgi_span2.data["http"]["method"] == "GET" @@ -503,7 +503,7 @@ def test_302_redirect(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span1.n == "urllib3" assert urllib3_span1.data["http"]["status"] == 200 - assert urllib3_span1.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span1.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span1.data["http"]["method"] == "GET" assert urllib3_span1.stack assert isinstance(urllib3_span1.stack, list) @@ -511,7 +511,7 @@ def test_302_redirect(self): assert urllib3_span2.n == "urllib3" assert urllib3_span2.data["http"]["status"] == 302 - assert urllib3_span2.data["http"]["url"] == testenv["wsgi_server"] + "/302" + assert urllib3_span2.data["http"]["url"] == testenv["flask_server"] + "/302" assert urllib3_span2.data["http"]["method"] == "GET" assert urllib3_span2.stack assert isinstance(urllib3_span2.stack, list) @@ -519,7 +519,7 @@ def test_302_redirect(self): def test_5xx_request(self): with tracer.start_as_current_span("test"): - r = self.http.request("GET", testenv["wsgi_server"] + "/504") + r = self.http.request("GET", testenv["flask_server"] + "/504") spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -549,7 +549,7 @@ def test_5xx_request(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/504" assert wsgi_span.data["http"]["method"] == "GET" @@ -561,7 +561,7 @@ def test_5xx_request(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 504 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/504" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/504" assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -570,7 +570,7 @@ def test_5xx_request(self): def test_exception_logging(self): with tracer.start_as_current_span("test"): try: - r = self.http.request("GET", testenv["wsgi_server"] + "/exception") + r = self.http.request("GET", testenv["flask_server"] + "/exception") except Exception: pass @@ -615,7 +615,7 @@ def test_exception_logging(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/exception" assert wsgi_span.data["http"]["method"] == "GET" @@ -630,7 +630,7 @@ def test_exception_logging(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 500 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/exception" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/exception" assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -681,7 +681,7 @@ def test_requests_pkg_get(self): self.recorder.clear_spans() with tracer.start_as_current_span("test"): - r = requests.get(testenv["wsgi_server"] + "/", timeout=2) + r = requests.get(testenv["flask_server"] + "/", timeout=2) spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -710,7 +710,7 @@ def test_requests_pkg_get(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/" assert wsgi_span.data["http"]["method"] == "GET" @@ -722,7 +722,7 @@ def test_requests_pkg_get(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -734,7 +734,7 @@ def test_requests_pkg_get_with_custom_headers(self): with tracer.start_as_current_span("test"): r = requests.get( - testenv["wsgi_server"] + "/", timeout=2, headers=my_custom_headers + testenv["flask_server"] + "/", timeout=2, headers=my_custom_headers ) spans = self.recorder.queued_spans() @@ -764,7 +764,7 @@ def test_requests_pkg_get_with_custom_headers(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/" assert wsgi_span.data["http"]["method"] == "GET" @@ -776,7 +776,7 @@ def test_requests_pkg_get_with_custom_headers(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -784,7 +784,7 @@ def test_requests_pkg_get_with_custom_headers(self): def test_requests_pkg_put(self): with tracer.start_as_current_span("test"): - r = requests.put(testenv["wsgi_server"] + "/notfound") + r = requests.put(testenv["flask_server"] + "/notfound") spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -812,7 +812,7 @@ def test_requests_pkg_put(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/notfound" assert wsgi_span.data["http"]["method"] == "PUT" @@ -824,7 +824,7 @@ def test_requests_pkg_put(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 404 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/notfound" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/notfound" assert urllib3_span.data["http"]["method"] == "PUT" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) @@ -835,7 +835,7 @@ def test_response_header_capture(self): agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] with tracer.start_as_current_span("test"): - r = self.http.request("GET", testenv["wsgi_server"] + "/response_headers") + r = self.http.request("GET", testenv["flask_server"] + "/response_headers") spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -864,7 +864,7 @@ def test_response_header_capture(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/response_headers" assert wsgi_span.data["http"]["method"] == "GET" @@ -878,7 +878,7 @@ def test_response_header_capture(self): assert urllib3_span.data["http"]["status"] == 200 assert ( urllib3_span.data["http"]["url"] - == testenv["wsgi_server"] + "/response_headers" + == testenv["flask_server"] + "/response_headers" ) assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack @@ -902,7 +902,7 @@ def test_request_header_capture(self): } with tracer.start_as_current_span("test"): r = self.http.request( - "GET", testenv["wsgi_server"] + "/", headers=request_headers + "GET", testenv["flask_server"] + "/", headers=request_headers ) spans = self.recorder.queued_spans() @@ -932,7 +932,7 @@ def test_request_header_capture(self): # wsgi assert wsgi_span.n == "wsgi" assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert wsgi_span.data["http"]["url"] == "/" assert wsgi_span.data["http"]["method"] == "GET" @@ -944,7 +944,7 @@ def test_request_header_capture(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == testenv["wsgi_server"] + "/" + assert urllib3_span.data["http"]["url"] == testenv["flask_server"] + "/" assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) diff --git a/tests/frameworks/test_aiohttp_client.py b/tests/frameworks/test_aiohttp_client.py index 0fc9455f..72efc437 100644 --- a/tests/frameworks/test_aiohttp_client.py +++ b/tests/frameworks/test_aiohttp_client.py @@ -38,7 +38,7 @@ def test_client_get(self): async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") response = self.loop.run_until_complete(test()) @@ -67,7 +67,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", + self.assertEqual(testenv["flask_server"] + "/", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertIsNotNone(aiohttp_span.stack) @@ -88,7 +88,7 @@ def test_client_get_as_root_exit_span(self): agent.options.allow_exit_as_root = True async def test(): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") response = self.loop.run_until_complete(test()) @@ -117,7 +117,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", + self.assertEqual(testenv["flask_server"] + "/", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertIsNotNone(aiohttp_span.stack) @@ -138,7 +138,7 @@ def test_client_get_301(self): async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/301") + return await self.fetch(session, testenv["flask_server"] + "/301") response = self.loop.run_until_complete(test()) @@ -171,7 +171,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/301", + self.assertEqual(testenv["flask_server"] + "/301", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertIsNotNone(aiohttp_span.stack) @@ -192,7 +192,7 @@ def test_client_get_405(self): async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/405") + return await self.fetch(session, testenv["flask_server"] + "/405") response = self.loop.run_until_complete(test()) @@ -221,7 +221,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(405, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/405", + self.assertEqual(testenv["flask_server"] + "/405", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertIsNotNone(aiohttp_span.stack) @@ -242,7 +242,7 @@ def test_client_get_500(self): async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/500") + return await self.fetch(session, testenv["flask_server"] + "/500") response = self.loop.run_until_complete(test()) @@ -271,7 +271,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(500, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/500", + self.assertEqual(testenv["flask_server"] + "/500", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertEqual('INTERNAL SERVER ERROR', @@ -294,7 +294,7 @@ def test_client_get_504(self): async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/504") + return await self.fetch(session, testenv["flask_server"] + "/504") response = self.loop.run_until_complete(test()) @@ -323,7 +323,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(504, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/504", + self.assertEqual(testenv["flask_server"] + "/504", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertEqual('GATEWAY TIMEOUT', aiohttp_span.data["http"]["error"]) @@ -345,7 +345,7 @@ def test_client_get_with_params_to_scrub(self): async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"], params={"secret": "yeah"}) + return await self.fetch(session, testenv["flask_server"], params={"secret": "yeah"}) response = self.loop.run_until_complete(test()) @@ -374,7 +374,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", + self.assertEqual(testenv["flask_server"] + "/", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertEqual("secret=", @@ -400,7 +400,7 @@ def test_client_response_header_capture(self): async def test(): with async_tracer.start_active_span('test'): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/response_headers") + return await self.fetch(session, testenv["flask_server"] + "/response_headers") response = self.loop.run_until_complete(test()) @@ -429,7 +429,7 @@ async def test(): self.assertEqual("aiohttp-client", aiohttp_span.n) self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/response_headers", aiohttp_span.data["http"]["url"]) + self.assertEqual(testenv["flask_server"] + "/response_headers", aiohttp_span.data["http"]["url"]) self.assertEqual("GET", aiohttp_span.data["http"]["method"]) self.assertIsNotNone(aiohttp_span.stack) self.assertTrue(type(aiohttp_span.stack) is list) diff --git a/tests/frameworks/test_asyncio.py b/tests/frameworks/test_asyncio.py index 73bcf95b..97483616 100644 --- a/tests/frameworks/test_asyncio.py +++ b/tests/frameworks/test_asyncio.py @@ -39,7 +39,7 @@ def test_ensure_future_with_context(self): async def run_later(msg="Hello"): # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): with async_tracer.start_active_span('test'): @@ -69,7 +69,7 @@ def test_ensure_future_without_context(self): async def run_later(msg="Hello"): # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): with async_tracer.start_active_span('test'): @@ -92,7 +92,7 @@ def test_create_task_with_context(self): async def run_later(msg="Hello"): # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): with async_tracer.start_active_span('test'): @@ -122,7 +122,7 @@ def test_create_task_without_context(self): async def run_later(msg="Hello"): # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") + return await self.fetch(session, testenv["flask_server"] + "/") async def test(): with async_tracer.start_active_span('test'): diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 22627f47..da1fd3c2 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -36,7 +36,7 @@ def tearDown(self) -> None: return None def test_vanilla_requests(self) -> None: - r = self.http.request('GET', testenv["wsgi_server"] + '/') + r = self.http.request('GET', testenv["flask_server"] + '/') assert r.status == 200 spans = self.recorder.queued_spans() @@ -44,7 +44,7 @@ def test_vanilla_requests(self) -> None: def test_get_request(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request("GET", testenv["wsgi_server"] + "/") + response = self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -94,7 +94,7 @@ def test_get_request(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str( - testenv["wsgi_port"] + testenv["flask_port"] ) assert "/" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -106,7 +106,7 @@ def test_get_request(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert 200 == urllib3_span.data["http"]["status"] - assert testenv["wsgi_server"] + "/" == urllib3_span.data["http"]["url"] + assert testenv["flask_server"] + "/" == urllib3_span.data["http"]["url"] assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list @@ -118,7 +118,7 @@ def test_get_request(self) -> None: def test_get_request_with_query_params(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request( - "GET", testenv["wsgi_server"] + "/" + "?key1=val1&key2=val2" + "GET", testenv["flask_server"] + "/" + "?key1=val1&key2=val2" ) spans = self.recorder.queued_spans() @@ -169,7 +169,7 @@ def test_get_request_with_query_params(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/" == wsgi_span.data["http"]["url"] assert wsgi_span.data["http"]["params"] == "key1=&key2=" @@ -182,7 +182,7 @@ def test_get_request_with_query_params(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert 200 == urllib3_span.data["http"]["status"] - assert testenv["wsgi_server"] + "/" == urllib3_span.data["http"]["url"] + assert testenv["flask_server"] + "/" == urllib3_span.data["http"]["url"] assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list @@ -194,7 +194,7 @@ def test_get_request_with_query_params(self) -> None: @unittest.skip("Suppression is not yet handled") def test_get_request_with_suppression(self) -> None: headers = {'X-INSTANA-L':'0'} - response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers) + response = self.http.urlopen('GET', testenv["flask_server"] + '/', headers=headers) spans = self.recorder.queued_spans() @@ -220,7 +220,7 @@ def test_get_request_with_suppression_and_w3c(self) -> None: 'traceparent': '00-0af7651916cd43dd8448eb211c80319c-b9c7c989f97918e1-01', 'tracestate': 'congo=ucfJifl5GOE,rojo=00f067aa0ba902b7'} - response = self.http.urlopen('GET', testenv["wsgi_server"] + '/', headers=headers) + response = self.http.urlopen('GET', testenv["flask_server"] + '/', headers=headers) spans = self.recorder.queued_spans() @@ -246,7 +246,7 @@ def test_synthetic_request(self) -> None: } with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=headers) + response = self.http.request('GET', testenv["flask_server"] + '/', headers=headers) spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -261,7 +261,7 @@ def test_synthetic_request(self) -> None: def test_render_template(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/render') + response = self.http.request('GET', testenv["flask_server"] + '/render') spans = self.recorder.queued_spans() assert len(spans) == 4 @@ -318,7 +318,7 @@ def test_render_template(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/render" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -330,7 +330,7 @@ def test_render_template(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert 200 == urllib3_span.data["http"]["status"] - assert testenv["wsgi_server"] + "/render" == urllib3_span.data["http"]["url"] + assert testenv["flask_server"] + "/render" == urllib3_span.data["http"]["url"] assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list @@ -341,7 +341,7 @@ def test_render_template(self) -> None: def test_render_template_string(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/render_string') + response = self.http.request('GET', testenv["flask_server"] + '/render_string') spans = self.recorder.queued_spans() assert len(spans) == 4 @@ -398,7 +398,7 @@ def test_render_template_string(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/render_string" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -411,7 +411,7 @@ def test_render_template_string(self) -> None: assert "urllib3" == urllib3_span.n assert 200 == urllib3_span.data["http"]["status"] assert ( - testenv["wsgi_server"] + "/render_string" + testenv["flask_server"] + "/render_string" == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] @@ -424,7 +424,7 @@ def test_render_template_string(self) -> None: def test_301(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/301', redirect=False) + response = self.http.request('GET', testenv["flask_server"] + '/301', redirect=False) spans = self.recorder.queued_spans() @@ -470,7 +470,7 @@ def test_301(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/301" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -482,7 +482,7 @@ def test_301(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert 301 == urllib3_span.data["http"]["status"] - assert testenv["wsgi_server"] + "/301" == urllib3_span.data["http"]["url"] + assert testenv["flask_server"] + "/301" == urllib3_span.data["http"]["url"] assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list @@ -493,7 +493,7 @@ def test_301(self) -> None: def test_custom_404(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/custom-404') + response = self.http.request('GET', testenv["flask_server"] + '/custom-404') spans = self.recorder.queued_spans() @@ -539,7 +539,7 @@ def test_custom_404(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/custom-404" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -552,7 +552,7 @@ def test_custom_404(self) -> None: assert "urllib3" == urllib3_span.n assert 404 == urllib3_span.data["http"]["status"] assert ( - testenv["wsgi_server"] + "/custom-404" == urllib3_span.data["http"]["url"] + testenv["flask_server"] + "/custom-404" == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None @@ -564,7 +564,7 @@ def test_custom_404(self) -> None: def test_404(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/11111111111') + response = self.http.request('GET', testenv["flask_server"] + '/11111111111') spans = self.recorder.queued_spans() @@ -610,7 +610,7 @@ def test_404(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/11111111111" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -623,7 +623,7 @@ def test_404(self) -> None: assert "urllib3" == urllib3_span.n assert 404 == urllib3_span.data["http"]["status"] assert ( - testenv["wsgi_server"] + "/11111111111" == urllib3_span.data["http"]["url"] + testenv["flask_server"] + "/11111111111" == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None @@ -635,7 +635,7 @@ def test_404(self) -> None: def test_500(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/500') + response = self.http.request('GET', testenv["flask_server"] + '/500') spans = self.recorder.queued_spans() @@ -681,7 +681,7 @@ def test_500(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/500" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -693,7 +693,7 @@ def test_500(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert 500 == urllib3_span.data["http"]["status"] - assert testenv["wsgi_server"] + "/500" == urllib3_span.data["http"]["url"] + assert testenv["flask_server"] + "/500" == urllib3_span.data["http"]["url"] assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list @@ -707,7 +707,7 @@ def test_render_error(self) -> None: raise unittest.SkipTest("Exceptions without handlers vary with blinker") with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/render_error') + response = self.http.request('GET', testenv["flask_server"] + '/render_error') spans = self.recorder.queued_spans() @@ -762,7 +762,7 @@ def test_render_error(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/render_error" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -775,7 +775,7 @@ def test_render_error(self) -> None: assert "urllib3" == urllib3_span.n assert 500 == urllib3_span.data["http"]["status"] assert ( - testenv["wsgi_server"] + "/render_error" == urllib3_span.data["http"]["url"] + testenv["flask_server"] + "/render_error" == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None @@ -790,7 +790,7 @@ def test_exception(self) -> None: raise unittest.SkipTest("Exceptions without handlers vary with blinker") with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/exception') + response = self.http.request('GET', testenv["flask_server"] + '/exception') spans = self.recorder.queued_spans() @@ -829,7 +829,7 @@ def test_exception(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/exception" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -841,7 +841,7 @@ def test_exception(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert 500 == urllib3_span.data["http"]["status"] - assert testenv["wsgi_server"] + "/exception" == urllib3_span.data["http"]["url"] + assert testenv["flask_server"] + "/exception" == urllib3_span.data["http"]["url"] assert "GET" == urllib3_span.data["http"]["method"] assert urllib3_span.stack is not None assert type(urllib3_span.stack) is list @@ -852,7 +852,7 @@ def test_exception(self) -> None: def test_custom_exception_with_log(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/exception-invalid-usage') + response = self.http.request('GET', testenv["flask_server"] + '/exception-invalid-usage') spans = self.recorder.queued_spans() @@ -908,7 +908,7 @@ def test_custom_exception_with_log(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/exception-invalid-usage" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -921,7 +921,7 @@ def test_custom_exception_with_log(self) -> None: assert "urllib3" == urllib3_span.n assert 502 == urllib3_span.data["http"]["status"] assert ( - testenv["wsgi_server"] + "/exception-invalid-usage" + testenv["flask_server"] + "/exception-invalid-usage" == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] @@ -934,7 +934,7 @@ def test_custom_exception_with_log(self) -> None: def test_path_templates(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/users/Ricky/sayhello') + response = self.http.request('GET', testenv["flask_server"] + '/users/Ricky/sayhello') spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -979,7 +979,7 @@ def test_path_templates(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/users/Ricky/sayhello" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -992,7 +992,7 @@ def test_path_templates(self) -> None: assert "urllib3" == urllib3_span.n assert 200 == urllib3_span.data["http"]["status"] assert ( - testenv["wsgi_server"] + "/users/Ricky/sayhello" + testenv["flask_server"] + "/users/Ricky/sayhello" == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] @@ -1010,7 +1010,7 @@ def test_response_header_capture(self) -> None: agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/response_headers') + response = self.http.request('GET', testenv["flask_server"] + '/response_headers') spans = self.recorder.queued_spans() assert len(spans) == 3 @@ -1061,7 +1061,7 @@ def test_response_header_capture(self) -> None: assert "urllib3" == urllib3_span.n assert 200 == urllib3_span.data["http"]["status"] assert ( - testenv["wsgi_server"] + "/response_headers" + testenv["flask_server"] + "/response_headers" == urllib3_span.data["http"]["url"] ) assert "GET" == urllib3_span.data["http"]["method"] @@ -1072,7 +1072,7 @@ def test_response_header_capture(self) -> None: # wsgi assert "wsgi" == wsgi_span.n assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/response_headers" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] @@ -1094,7 +1094,7 @@ def test_request_started_exception(self) -> None: "instana.singletons.tracer.extract", side_effect=Exception("mocked error"), ): - self.http.request("GET", testenv["wsgi_server"] + "/") + self.http.request("GET", testenv["flask_server"] + "/") spans = self.recorder.queued_spans() assert len(spans) == 2 @@ -1105,7 +1105,7 @@ def test_request_started_exception(self) -> None: ) def test_got_request_exception(self) -> None: response = self.http.request( - "GET", testenv["wsgi_server"] + "/got_request_exception" + "GET", testenv["flask_server"] + "/got_request_exception" ) spans = self.recorder.queued_spans() @@ -1124,7 +1124,7 @@ def test_got_request_exception(self) -> None: # wsgi assert wsgi_span.n == "wsgi" assert ( - "127.0.0.1:" + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] + "127.0.0.1:" + str(testenv["flask_port"]) == wsgi_span.data["http"]["host"] ) assert "/got_request_exception" == wsgi_span.data["http"]["url"] assert "GET" == wsgi_span.data["http"]["method"] diff --git a/tests/frameworks/test_gevent.py b/tests/frameworks/test_gevent.py index 71a724ad..69a9a6c8 100644 --- a/tests/frameworks/test_gevent.py +++ b/tests/frameworks/test_gevent.py @@ -18,7 +18,7 @@ @unittest.skipIf(not os.environ.get("GEVENT_STARLETTE_TEST"), reason="") class TestGEvent(unittest.TestCase): def setUp(self): - self.http = urllib3.HTTPConnectionPool('127.0.0.1', port=testenv["wsgi_port"], maxsize=20) + self.http = urllib3.HTTPConnectionPool('127.0.0.1', port=testenv["flask_port"], maxsize=20) self.recorder = tracer.recorder self.recorder.clear_spans() tracer._scope_manager = GeventScopeManager() @@ -28,7 +28,7 @@ def tearDown(self): pass def make_http_call(self, n=None): - return self.http.request('GET', testenv["wsgi_server"] + '/') + return self.http.request('GET', testenv["flask_server"] + '/') def spawn_calls(self): with tracer.start_active_span('spawn_calls'): diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index df2c9fcc..8498dd45 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -79,7 +79,7 @@ def test_get_request(self) -> None: # wsgi assert "wsgi" == wsgi_span.n - assert '127.0.0.1:' + str(testenv['wsgi_port']) == wsgi_span.data["http"]["host"] + assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] assert '/' == wsgi_span.data["http"]["path"] assert 'GET' == wsgi_span.data["http"]["method"] assert "200" == wsgi_span.data["http"]["status"] @@ -161,7 +161,7 @@ def test_custom_header_capture(self) -> None: # wsgi assert "wsgi" == wsgi_span.n - assert '127.0.0.1:' + str(testenv['wsgi_port']) == wsgi_span.data["http"]["host"] + assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] assert '/' == wsgi_span.data["http"]["path"] assert 'GET' == wsgi_span.data["http"]["method"] assert "200" == wsgi_span.data["http"]["status"] @@ -219,7 +219,7 @@ def test_secret_scrubbing(self) -> None: # wsgi assert "wsgi" == wsgi_span.n - assert '127.0.0.1:' + str(testenv['wsgi_port']) == wsgi_span.data["http"]["host"] + assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] assert '/' == wsgi_span.data["http"]["path"] assert 'secret=' == wsgi_span.data["http"]["params"] assert 'GET' == wsgi_span.data["http"]["method"] From 81fc1e4b1a5de6d4970d2f77b2ca6e1cc42f5b69 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 16 Aug 2024 19:51:53 +0530 Subject: [PATCH 094/172] tests: Add legacy-cgi as requirement for bottle_wsgi in py 3.13 Signed-off-by: Varsha GS --- tests/requirements-313.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index 2f927181..86a13b95 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -16,6 +16,7 @@ markupsafe>=2.1.0 # Depends on grpcio #google-cloud-pubsub<=2.1.0 #google-cloud-storage>=1.24.0 +legacy-cgi>=2.6.1 lxml>=4.9.2 mock>=4.0.3 moto>=4.1.2 From 2dcf60dae232617d76ded019075ac41b6fda5620 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 19 Aug 2024 21:14:27 +0530 Subject: [PATCH 095/172] wsgi: minor fixes Signed-off-by: Varsha GS --- src/instana/middleware.py | 2 +- tests/apps/bottle_app/__init__.py | 2 ++ tests/frameworks/test_wsgi.py | 2 +- tests/requirements-313.txt | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/instana/middleware.py b/src/instana/middleware.py index 71fa0efa..49231a26 100644 --- a/src/instana/middleware.py +++ b/src/instana/middleware.py @@ -3,4 +3,4 @@ from instana.instrumentation.wsgi import InstanaWSGIMiddleware -# from .instrumentation.asgi import InstanaASGIMiddleware +# from instana.instrumentation.asgi import InstanaASGIMiddleware diff --git a/tests/apps/bottle_app/__init__.py b/tests/apps/bottle_app/__init__.py index 0aa902b2..44cdabb1 100644 --- a/tests/apps/bottle_app/__init__.py +++ b/tests/apps/bottle_app/__init__.py @@ -1,3 +1,5 @@ +# (c) Copyright IBM Corp. 2024 + import os from tests.apps.bottle_app.app import bottle_server as server from tests.apps.utils import launch_background_thread diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index 8498dd45..186d84f7 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -14,7 +14,7 @@ class TestWSGI: @pytest.fixture(autouse=True) - def _setUp(self) -> Generator[None, None, None]: + def _resource(self) -> Generator[None, None, None]: """ Clear all spans before a test run """ self.http = urllib3.PoolManager() self.recorder = tracer.span_processor diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index 86a13b95..0f701231 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -16,6 +16,8 @@ markupsafe>=2.1.0 # Depends on grpcio #google-cloud-pubsub<=2.1.0 #google-cloud-storage>=1.24.0 +# The `legacy-cgi` package is a drop-in replacement for the `cgi` package, +# which was removed from Python 3.13 onwards. `Bottle` framework still uses `cgi`. legacy-cgi>=2.6.1 lxml>=4.9.2 mock>=4.0.3 From b2cc66dca4e35fd6a249519e1aa28963c2a2f19a Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 12 Aug 2024 16:55:46 +0200 Subject: [PATCH 096/172] refactor: ASGI instrumentation Signed-off-by: Paulo Vital --- src/instana/instrumentation/asgi.py | 154 +++++++++------- src/instana/middleware.py | 4 +- src/instana/propagators/base_propagator.py | 175 ++++++++++++------- src/instana/propagators/binary_propagator.py | 8 +- src/instana/tracer.py | 4 +- 5 files changed, 207 insertions(+), 138 deletions(-) diff --git a/src/instana/instrumentation/asgi.py b/src/instana/instrumentation/asgi.py index 27c20e9e..8e2fb825 100644 --- a/src/instana/instrumentation/asgi.py +++ b/src/instana/instrumentation/asgi.py @@ -4,11 +4,20 @@ """ Instana ASGI Middleware """ -import opentracing -from ..log import logger -from ..singletons import async_tracer, agent -from ..util.secrets import strip_secrets_from_query +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict + +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind + +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query + +if TYPE_CHECKING: + from starlette.middleware.exceptions import ExceptionMiddleware + from instana.span.span import InstanaSpan class InstanaASGIMiddleware: @@ -16,94 +25,115 @@ class InstanaASGIMiddleware: Instana ASGI Middleware """ - def __init__(self, app): + def __init__(self, app: "ExceptionMiddleware") -> None: self.app = app - def _extract_custom_headers(self, span, headers): + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any] + ) -> None: if agent.options.extra_http_headers is None: - return + return try: for custom_header in agent.options.extra_http_headers: # Headers are in the following format: b'x-header-1' for header_pair in headers: - if header_pair[0].decode('utf-8').lower() == custom_header.lower(): - span.set_tag("http.header.%s" % custom_header, header_pair[1].decode('utf-8')) + if header_pair[0].decode("utf-8").lower() == custom_header.lower(): + span.set_attribute( + "http.header.%s" % custom_header, + header_pair[1].decode("utf-8"), + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - def _collect_kvs(self, scope, span): + def _collect_kvs(self, scope: Dict[str, Any], span: "InstanaSpan") -> None: try: - span.set_tag('span.kind', 'entry') - span.set_tag('http.path', scope.get('path')) - span.set_tag('http.method', scope.get('method')) + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute("http.path", scope.get("path")) + span.set_attribute("http.method", scope.get("method")) - server = scope.get('server') - if isinstance(server, tuple): - span.set_tag('http.host', server[0]) + server = scope.get("server") + if isinstance(server, tuple) or isinstance(server, list): + span.set_attribute("http.host", server[0]) - query = scope.get('query_string') + query = scope.get("query_string") if isinstance(query, (str, bytes)) and len(query): if isinstance(query, bytes): - query = query.decode('utf-8') - scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, - agent.options.secrets_list) - span.set_tag("http.params", scrubbed_params) - - app = scope.get('app') - if app is not None and hasattr(app, 'routes'): + query = query.decode("utf-8") + scrubbed_params = strip_secrets_from_query( + query, agent.options.secrets_matcher, agent.options.secrets_list + ) + span.set_attribute("http.params", scrubbed_params) + + app = scope.get("app") + if app and hasattr(app, "routes"): # Attempt to detect the Starlette routes registered. # If Starlette isn't present, we harmlessly dump out. from starlette.routing import Match - for route in scope['app'].routes: + + for route in scope["app"].routes: if route.matches(scope)[0] == Match.FULL: - span.set_tag("http.path_tpl", route.path) + span.set_attribute("http.path_tpl", route.path) except Exception: logger.debug("ASGI collect_kvs: ", exc_info=True) - async def __call__(self, scope, receive, send): + async def __call__( + self, + scope: Dict[str, Any], + receive: Callable[[], Awaitable[Dict[str, Any]]], + send: Callable[[Dict[str, Any]], Awaitable[None]], + ) -> None: request_context = None if scope["type"] not in ("http", "websocket"): - await self.app(scope, receive, send) - return + return await self.app(scope, receive, send) - request_headers = scope.get('headers') + request_headers = scope.get("headers") if isinstance(request_headers, list): - request_context = async_tracer.extract(opentracing.Format.BINARY, request_headers) + request_context = tracer.extract(Format.BINARY, request_headers) - async def send_wrapper(response): - span = async_tracer.active_span - if span is None: - await send(response) - else: - if response['type'] == 'http.response.start': - try: - status_code = response.get('status') - if status_code is not None: - if 500 <= int(status_code): - span.mark_as_errored() - span.set_tag('http.status_code', status_code) - - headers = response.get('headers') - if headers is not None: - self._extract_custom_headers(span, headers) - async_tracer.inject(span.context, opentracing.Format.BINARY, headers) - except Exception: - logger.debug("send_wrapper: ", exc_info=True) + with tracer.start_as_current_span("asgi", span_context=request_context) as span: + self._collect_kvs(scope, span) + if "headers" in scope and agent.options.extra_http_headers: + self._extract_custom_headers(span, scope["headers"]) - try: - await send(response) - except Exception as exc: - span.log_exception(exc) - raise - - with async_tracer.start_active_span("asgi", child_of=request_context) as tracing_scope: - self._collect_kvs(scope, tracing_scope.span) - if 'headers' in scope and agent.options.extra_http_headers is not None: - self._extract_custom_headers(tracing_scope.span, scope['headers']) + instana_send = self._send_with_instana( + span, + scope, + send, + ) try: - await self.app(scope, receive, send_wrapper) + await self.app(scope, receive, instana_send) except Exception as exc: - tracing_scope.span.log_exception(exc) + span.record_exception(exc) raise exc + + def _send_with_instana( + self, + current_span: "InstanaSpan", + scope: Dict[str, Any], + send: Callable[[Dict[str, Any]], Awaitable[None]], + ) -> Awaitable[None]: + async def send_wrapper(response: Dict[str, Any]) -> Awaitable[None]: + if response["type"] == "http.response.start": + try: + status_code = response.get("status") + if status_code: + if 500 <= int(status_code): + current_span.mark_as_errored() + current_span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) + + headers = response.get("headers") + if headers: + self._extract_custom_headers(current_span, headers) + tracer.inject(current_span.context, Format.BINARY, headers) + except Exception: + logger.debug("ASGI send_wrapper error: ", exc_info=True) + + try: + await send(response) + except Exception as exc: + current_span.record_exception(exc) + raise + + return send_wrapper diff --git a/src/instana/middleware.py b/src/instana/middleware.py index 49231a26..ef9be47d 100644 --- a/src/instana/middleware.py +++ b/src/instana/middleware.py @@ -2,5 +2,5 @@ # (c) Copyright Instana Inc. 2017 -from instana.instrumentation.wsgi import InstanaWSGIMiddleware -# from instana.instrumentation.asgi import InstanaASGIMiddleware +from instana.instrumentation.asgi import InstanaASGIMiddleware # noqa: F401 +from instana.instrumentation.wsgi import InstanaWSGIMiddleware # noqa: F401 diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index a6a1add3..eb779d1d 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -3,7 +3,8 @@ import os -import typing + +from typing import Any, Optional, TypeVar, Dict, List, Tuple from instana.log import logger from instana.util.ids import header_to_id, header_to_long_id @@ -27,7 +28,7 @@ # # For injection, we only support the standard format: # X-Instana-T -CarrierT = typing.TypeVar("CarrierT", typing.Dict, typing.List, typing.Tuple) +CarrierT = TypeVar("CarrierT", Dict, List, Tuple) class BasePropagator(object): @@ -71,12 +72,14 @@ def __init__(self): self._ts = Tracestate() @staticmethod - def extract_headers_dict(carrier): + def extract_headers_dict(carrier: CarrierT) -> Optional[Dict]: """ - This method converts the incoming carrier into a dict - :param carrier: - :return: dc dictionary + This method converts the incoming carrier into a dict. + + :param carrier: CarrierT + :return: Dict | None """ + dc = None try: if isinstance(carrier, dict): dc = carrier @@ -85,17 +88,17 @@ def extract_headers_dict(carrier): else: dc = dict(carrier) except Exception: - logger.debug("extract: Couldn't convert %s", carrier) - dc = None + logger.debug(f"base_propagator extract_headers_dict: Couldn't convert - {carrier}") return dc @staticmethod - def _get_ctx_level(level): + def _get_ctx_level(level: str) -> int: """ - Extract the level value and return it, as it may include correlation values - :param level: - :return: + Extract the level value and return it, as it may include correlation values. + + :param level: str + :return: int """ try: ctx_level = int(level.split(",")[0]) if level else 1 @@ -104,24 +107,28 @@ def _get_ctx_level(level): return ctx_level @staticmethod - def _set_correlation_properties(level, ctx): + def _get_correlation_properties(level:str): """ - Set the correlation values if they are present - :param level: - :param ctx: - :return: + Get the correlation values if they are present. + + :param level: str + :return: Tuple[Any, Any] - correlation_type, correlation_id """ + correlation_type, correlation_id = [None] * 2 try: - ctx.correlation_type = level.split(",")[1].split("correlationType=")[1].split(";")[0] + correlation_type = level.split(",")[1].split("correlationType=")[1].split(";")[0] if "correlationId" in level: - ctx.correlation_id = level.split(",")[1].split("correlationId=")[1].split(";")[0] + correlation_id = level.split(",")[1].split("correlationId=")[1].split(";")[0] except Exception: logger.debug("extract instana correlation type/id error:", exc_info=True) + + return correlation_type, correlation_id - def _get_participating_trace_context(self, span_context): + def _get_participating_trace_context(self, span_context: SpanContext): """ - This method is called for getting the updated traceparent and tracestate values - :param span_context: + This method is called for getting the updated traceparent and tracestate values. + + :param span_context: SpanContext :return: traceparent, tracestate """ if span_context.long_trace_id and not span_context.trace_parent: @@ -141,35 +148,53 @@ def _get_participating_trace_context(self, span_context): tracestate = self._ts.update_tracestate(tracestate, span_context.trace_id, span_context.span_id) return traceparent, tracestate - def __determine_span_context(self, trace_id, span_id, level, synthetic, traceparent, tracestate, - disable_w3c_trace_context): + def __determine_span_context( + self, + trace_id: int, + span_id: int, + level: str, + synthetic: bool, + traceparent, + tracestate, + disable_w3c_trace_context: bool, + ) -> SpanContext: """ This method determines the span context depending on a set of conditions being met Detailed description of the conditions can be found in the instana internal technical-documentation, - under section http-processing-for-instana-tracers - :param trace_id: instana trace id - :param span_id: instana span id - :param level: instana level - :param synthetic: instana synthetic + under section http-processing-for-instana-tracers. + + :param trace_id: int - instana trace id + :param span_id: int - instana span id + :param level: str - instana level + :param synthetic: bool - instana synthetic :param traceparent: :param tracestate: - :param disable_w3c_trace_context: flag used to enable w3c trace context only on HTTP requests - :return: ctx + :param disable_w3c_trace_context: bool - flag used to enable w3c trace context only on HTTP requests + :return: SpanContext """ correlation = False disable_traceparent = os.environ.get("INSTANA_DISABLE_W3C_TRACE_CORRELATION", "") instana_ancestor = None - ctx = SpanContext(trace_id=trace_id, span_id=span_id, is_remote=False) + if level and "correlationType" in level: trace_id, span_id = [None] * 2 correlation = True + ( + ctx_trace_id, + ctx_span_id, + ctx_level, + ctx_synthetic, + ctx_trace_parent, + ctx_instana_ancestor, + ctx_long_trace_id, + ctx_correlation_type, + ctx_correlation_id, + ctx_traceparent, + ctx_tracestate, + ) = [None] * 11 + ctx_level = self._get_ctx_level(level) - if ctx_level == 0 or level == '0': - trace_id = ctx.trace_id = None - span_id = ctx.span_id = None - ctx.correlation_type = None - ctx.correlation_id = None if ( trace_id @@ -179,52 +204,64 @@ def __determine_span_context(self, trace_id, span_id, level, synthetic, tracepar ): # ctx.trace_id = trace_id[-16:] # only the last 16 chars # ctx.span_id = span_id[-16:] # only the last 16 chars - ctx.trace_id = trace_id - ctx.span_id = span_id - ctx.synthetic = synthetic is not None + ctx_trace_id = trace_id + ctx_span_id = span_id + ctx_synthetic = synthetic # if len(trace_id) > 16: - ctx.long_trace_id = trace_id + ctx_long_trace_id = trace_id - elif not disable_w3c_trace_context and traceparent and trace_id is None and span_id is None: + elif not disable_w3c_trace_context and traceparent and not trace_id and not span_id: _, tp_trace_id, tp_parent_id, _ = self._tp.get_traceparent_fields(traceparent) if tracestate and "in=" in tracestate: instana_ancestor = self._ts.get_instana_ancestor(tracestate) if disable_traceparent == "": - ctx.trace_id = tp_trace_id[-16:] - ctx.span_id = tp_parent_id - ctx.synthetic = synthetic is not None - ctx.trace_parent = True - ctx.instana_ancestor = instana_ancestor - ctx.long_trace_id = tp_trace_id + ctx_trace_id = tp_trace_id[-16:] + ctx_span_id = tp_parent_id + ctx_synthetic = synthetic + ctx_trace_parent = True + ctx_instana_ancestor = instana_ancestor + ctx_long_trace_id = tp_trace_id else: if instana_ancestor: - ctx.trace_id = instana_ancestor.t - ctx.span_id = instana_ancestor.p - ctx.synthetic = synthetic is not None + ctx_trace_id = instana_ancestor.t + ctx_span_id = instana_ancestor.p + ctx_synthetic = synthetic elif synthetic: - ctx.synthetic = synthetic + ctx_synthetic = synthetic if correlation: - self._set_correlation_properties(level, ctx) + ctx_correlation_type, ctx_correlation_id = self._get_correlation_properties(level) if traceparent: - ctx.traceparent = traceparent - ctx.tracestate = tracestate - - ctx.level = ctx_level - - return ctx - - def extract_instana_headers(self, dc): + ctx_traceparent = traceparent + ctx_tracestate = tracestate + + return SpanContext( + trace_id=ctx_trace_id, + span_id=ctx_span_id, + is_remote=False, + level=ctx_level, + synthetic=ctx_synthetic, + trace_parent=ctx_trace_parent, + instana_ancestor=ctx_instana_ancestor, + long_trace_id=ctx_long_trace_id, + correlation_type=ctx_correlation_type, + correlation_id=ctx_correlation_id, + traceparent=ctx_traceparent, + tracestate=ctx_tracestate, + ) + + + def extract_instana_headers(self, dc: Dict[str, Any]) -> Tuple[Optional[int], Optional[int], Optional[str], Optional[bool]]: """ - Search carrier for the *HEADER* keys and return the tracing key-values + Search carrier for the *HEADER* keys and return the tracing key-values. - :param dc: The dict or list potentially containing context - :return: trace_id, span_id, level, synthetic + :param dc: Dict - The dict potentially containing context + :return: Tuple[Optional[int], Optional[int], Optional[str], Optional[bool]] - trace_id, span_id, level, synthetic """ trace_id, span_id, level, synthetic = [None] * 4 @@ -282,10 +319,12 @@ def __extract_w3c_trace_context_headers(self, dc): return traceparent, tracestate - def extract(self, carrier, disable_w3c_trace_context=False): + def extract(self, carrier: CarrierT, disable_w3c_trace_context: bool = False) -> Optional[SpanContext]: """ - This method overrides one of the Baseclasses as with the introduction of W3C trace context for the HTTP - requests more extracting steps and logic was required + This method overrides one of the Base classes as with the introduction + of W3C trace context for the HTTP requests more extracting steps and + logic was required. + :param disable_w3c_trace_context: :param carrier: :return: the context or None @@ -321,4 +360,4 @@ def extract(self, carrier, disable_w3c_trace_context=False): return span_context except Exception: - logger.debug("extract error:", exc_info=True) + logger.debug("base_propagator extract error:", exc_info=True) diff --git a/src/instana/propagators/binary_propagator.py b/src/instana/propagators/binary_propagator.py index 92a294d9..89c9002f 100644 --- a/src/instana/propagators/binary_propagator.py +++ b/src/instana/propagators/binary_propagator.py @@ -25,10 +25,10 @@ def __init__(self): def inject(self, span_context, carrier, disable_w3c_trace_context=True): try: - trace_id = str.encode(span_context.trace_id) - span_id = str.encode(span_context.span_id) - level = str.encode(str(span_context.level)) - server_timing = str.encode("intid;desc=%s" % span_context.trace_id) + trace_id = str(span_context.trace_id).encode() + span_id = str(span_context.span_id).encode() + level = str(span_context.level).encode() + server_timing = f"intid;desc={span_context.trace_id}".encode() if disable_w3c_trace_context: traceparent, tracestate = [None] * 2 diff --git a/src/instana/tracer.py b/src/instana/tracer.py index fc5301f9..5f08f729 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -228,8 +228,8 @@ def _create_span_context(self, parent_context: SpanContext) -> SpanContext: span_id=span_id, trace_flags=trace_flags, is_remote=is_remote, - level=(parent_context.level if parent_context is not None else 1), - synthetic=False, + level=(parent_context.level if parent_context else 1), + synthetic=(parent_context.synthetic if parent_context else False), ) if parent_context is not None: From b0982b47619c187d23bcf88404ef73dc7fa2a9d0 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 12 Aug 2024 16:57:54 +0200 Subject: [PATCH 097/172] refactor: Starlette instrumentation. Signed-off-by: Paulo Vital --- src/instana/__init__.py | 2 +- src/instana/instrumentation/starlette_inst.py | 23 ++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 3db1ca49..4f0310e3 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -179,7 +179,7 @@ def boot_agent(): # pymysql, # noqa: F401 # redis, # noqa: F401 # sqlalchemy, # noqa: F401 - # starlette_inst, # noqa: F401 + starlette_inst, # noqa: F401 # sanic_inst, # noqa: F401 urllib3, # noqa: F401 ) diff --git a/src/instana/instrumentation/starlette_inst.py b/src/instana/instrumentation/starlette_inst.py index 66c3d0b3..4edf4b4e 100644 --- a/src/instana/instrumentation/starlette_inst.py +++ b/src/instana/instrumentation/starlette_inst.py @@ -5,18 +5,29 @@ Instrumentation for Starlette https://www.starlette.io/ """ + +from typing import Any, Callable, Dict, Tuple + +import starlette.applications + try: import starlette import wrapt - from ..log import logger - from .asgi import InstanaASGIMiddleware from starlette.middleware import Middleware - @wrapt.patch_function_wrapper('starlette.applications', 'Starlette.__init__') - def init_with_instana(wrapped, instance, args, kwargs): - middleware = kwargs.get('middleware') + from instana.instrumentation.asgi import InstanaASGIMiddleware + from instana.log import logger + + @wrapt.patch_function_wrapper("starlette.applications", "Starlette.__init__") + def init_with_instana( + wrapped: Callable[..., starlette.applications.Starlette.__init__], + instance: starlette.applications.Starlette, + args: Tuple, + kwargs: Dict[str, Any], + ) -> None: + middleware = kwargs.get("middleware") if middleware is None: - kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)] + kwargs["middleware"] = [Middleware(InstanaASGIMiddleware)] elif isinstance(middleware, list): middleware.append(Middleware(InstanaASGIMiddleware)) From 91f383fefb885db28448065414bcf09042202ab0 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 12 Aug 2024 16:58:33 +0200 Subject: [PATCH 098/172] tests(asgi+starlette): adapt tests to OTel usage. Signed-off-by: Paulo Vital --- .coveragerc | 2 + tests/apps/starlette_app/__init__.py | 21 +- tests/apps/starlette_app/app.py | 29 +- tests/apps/starlette_app/app2.py | 41 ++ tests/conftest.py | 21 +- tests/frameworks/test_starlette.py | 527 +++++++++--------- tests/frameworks/test_starlette_middleware.py | 143 +++++ tests/requirements-310.txt | 1 + tests/requirements-312.txt | 1 + tests/requirements-313.txt | 2 + tests/requirements-gevent-starlette.txt | 1 + tests/requirements.txt | 1 + 12 files changed, 511 insertions(+), 279 deletions(-) create mode 100644 tests/apps/starlette_app/app2.py create mode 100644 tests/frameworks/test_starlette_middleware.py diff --git a/.coveragerc b/.coveragerc index 88037559..084fd0bc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,3 +3,5 @@ exclude_lines = pragma: no cover if TYPE_CHECKING: except ImportError: + except Exception: + except Exception as exc: diff --git a/tests/apps/starlette_app/__init__.py b/tests/apps/starlette_app/__init__.py index 2b7653a4..6b46de1c 100644 --- a/tests/apps/starlette_app/__init__.py +++ b/tests/apps/starlette_app/__init__.py @@ -2,17 +2,28 @@ # (c) Copyright Instana Inc. 2020 import uvicorn -from ...helpers import testenv -from instana.log import logger +from tests.helpers import testenv + +testenv["starlette_host"] = "127.0.0.1" testenv["starlette_port"] = 10817 -testenv["starlette_server"] = ("http://127.0.0.1:" + str(testenv["starlette_port"])) +testenv["starlette_server"] = "http://" + testenv["starlette_host"] + ":" + str(testenv["starlette_port"]) + + def launch_starlette(): from .app import starlette_server from instana.singletons import agent # Hack together a manual custom headers list; We'll use this in tests - agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + ] - uvicorn.run(starlette_server, host='127.0.0.1', port=testenv['starlette_port'], log_level="critical") + uvicorn.run( + starlette_server, + host=testenv["starlette_host"], + port=testenv["starlette_port"], + log_level="critical", + ) diff --git a/tests/apps/starlette_app/app.py b/tests/apps/starlette_app/app.py index b7fdfec3..04878c12 100644 --- a/tests/apps/starlette_app/app.py +++ b/tests/apps/starlette_app/app.py @@ -1,35 +1,40 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +import os + from starlette.applications import Starlette from starlette.responses import PlainTextResponse -from starlette.routing import Route, Mount, WebSocketRoute +from starlette.routing import Mount, Route, WebSocketRoute from starlette.staticfiles import StaticFiles -import os dir_path = os.path.dirname(os.path.realpath(__file__)) + def homepage(request): - return PlainTextResponse('Hello, world!') + return PlainTextResponse("Hello, world!") + def user(request): - user_id = request.path_params['user_id'] - return PlainTextResponse('Hello, user id %s!' % user_id) + user_id = request.path_params["user_id"] + return PlainTextResponse("Hello, user id %s!" % user_id) + async def websocket_endpoint(websocket): await websocket.accept() - await websocket.send_text('Hello, websocket!') + await websocket.send_text("Hello, websocket!") await websocket.close() + def startup(): - print('Ready to go') + print("Ready to go") routes = [ - Route('/', homepage), - Route('/users/{user_id}', user), - WebSocketRoute('/ws', websocket_endpoint), - Mount('/static', StaticFiles(directory=dir_path + "/static")), + Route("/", homepage), + Route("/users/{user_id}", user), + WebSocketRoute("/ws", websocket_endpoint), + Mount("/static", StaticFiles(directory=dir_path + "/static")), ] -starlette_server = Starlette(debug=True, routes=routes, on_startup=[startup]) \ No newline at end of file +starlette_server = Starlette(debug=True, routes=routes, on_startup=[startup]) diff --git a/tests/apps/starlette_app/app2.py b/tests/apps/starlette_app/app2.py new file mode 100644 index 00000000..c3be2242 --- /dev/null +++ b/tests/apps/starlette_app/app2.py @@ -0,0 +1,41 @@ +# (c) Copyright IBM Corp. 2024 + +import os + +from starlette.applications import Starlette +from starlette.middleware import Middleware +from starlette.middleware.trustedhost import TrustedHostMiddleware +from starlette.responses import PlainTextResponse +from starlette.routing import Route + +dir_path = os.path.dirname(os.path.realpath(__file__)) + + +def homepage(request): + return PlainTextResponse("Hello, world!") + + +def five_hundred(request): + return PlainTextResponse("Something went wrong!", status_code=500) + + +def startup(): + print("Ready to go") + + +routes = [ + Route("/", homepage), + Route("/five", five_hundred), +] + +starlette_server = Starlette( + debug=True, + routes=routes, + on_startup=[startup], + middleware=[ + Middleware( + TrustedHostMiddleware, + allowed_hosts=["*"], + ), + ], +) diff --git a/tests/conftest.py b/tests/conftest.py index c47d61d7..6ba6741c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,7 @@ # codes are finalised. collect_ignore_glob.append("*clients/boto*") collect_ignore_glob.append("*clients/test_cassandra*") -collect_ignore_glob.append("*clients/test_counchbase*") +collect_ignore_glob.append("*clients/test_couchbase*") collect_ignore_glob.append("*clients/test_google*") collect_ignore_glob.append("*clients/test_mysql*") collect_ignore_glob.append("*clients/test_pika*") @@ -57,20 +57,19 @@ collect_ignore_glob.append("*frameworks/test_grpcio*") collect_ignore_glob.append("*frameworks/test_pyramid*") collect_ignore_glob.append("*frameworks/test_sanic*") -collect_ignore_glob.append("*frameworks/test_starlette*") collect_ignore_glob.append("*frameworks/test_tornado*") -# Cassandra and gevent tests are run in dedicated jobs on CircleCI and will -# be run explicitly. (So always exclude them here) -if not os.environ.get("CASSANDRA_TEST"): - collect_ignore_glob.append("*test_cassandra*") +# # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will +# # be run explicitly. (So always exclude them here) +# if not os.environ.get("CASSANDRA_TEST"): +# collect_ignore_glob.append("*test_cassandra*") -if not os.environ.get("COUCHBASE_TEST"): - collect_ignore_glob.append("*test_couchbase*") +# if not os.environ.get("COUCHBASE_TEST"): +# collect_ignore_glob.append("*test_couchbase*") -if not os.environ.get("GEVENT_STARLETTE_TEST"): - collect_ignore_glob.append("*test_gevent*") - collect_ignore_glob.append("*test_starlette*") +# if not os.environ.get("GEVENT_STARLETTE_TEST"): +# collect_ignore_glob.append("*test_gevent*") +# collect_ignore_glob.append("*test_starlette*") # Python 3.10 support is incomplete yet # TODO: Remove this once we start supporting Tornado >= 6.0 diff --git a/tests/frameworks/test_starlette.py b/tests/frameworks/test_starlette.py index ad67f4aa..7c15dd2b 100644 --- a/tests/frameworks/test_starlette.py +++ b/tests/frameworks/test_starlette.py @@ -1,275 +1,300 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import multiprocessing -import time +from typing import Generator + import pytest -import requests -import unittest - -from ..helpers import testenv -from instana.singletons import tracer -from ..helpers import get_first_span_by_filter - - -class TestStarlette(unittest.TestCase): - def setUp(self): - from tests.apps.starlette_app import launch_starlette - self.proc = multiprocessing.Process(target=launch_starlette, args=(), daemon=True) - self.proc.start() - time.sleep(2) - - def tearDown(self): - self.proc.kill() # Kill server after tests - - def test_vanilla_get(self): - result = requests.get(testenv["starlette_server"] + '/') - self.assertTrue(result) - spans = tracer.recorder.queued_spans() - # Starlette instrumentation (like all instrumentation) _always_ traces unless told otherwise - self.assertEqual(len(spans), 1) - self.assertEqual(spans[0].n, 'asgi') - - self.assertIn("X-INSTANA-T", result.headers) - self.assertIn("X-INSTANA-S", result.headers) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - - def test_basic_get(self): +from instana.singletons import agent, tracer +from starlette.testclient import TestClient + +from tests.apps.starlette_app.app import starlette_server +from tests.helpers import get_first_span_by_filter + + +class TestStarlette: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from Starlette to make it easier. + self.client = TestClient(starlette_server) + # Configure to capture custom headers + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + ] + # Clear all spans before a test run. + self.recorder = tracer.span_processor + self.recorder.clear_spans() + yield + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # Starlette instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: result = None - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/') - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_path_templates(self): - result = None - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/users/1') + assert asgi_span - self.assertTrue(result) + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' - test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + def test_path_templates(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/users/1", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual( result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/users/1') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/users/{user_id}') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_secret_scrubbing(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/users/1" + assert asgi_span.data["http"]["path_tpl"] == "/users/{user_id}" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_secret_scrubbing(self) -> None: result = None - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/?secret=shhh') - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - assert len(spans) == 3 - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/?secret=shhh", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertEqual(asgi_span.data['http']['params'], 'secret=') - - def test_synthetic_request(self): - request_headers = { - 'X-INSTANA-SYNTHETIC': '1' - } - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/', headers=request_headers) - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - assert len(spans) == 3 - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert asgi_span.data["http"]["params"] == "secret=" + + def test_synthetic_request(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-INSTANA-SYNTHETIC": "1", + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertTrue(asgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) - - def test_custom_header_capture(self): - from instana.singletons import agent - - # The background Starlette server is pre-configured with custom headers to capture - - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } - with tracer.start_active_span('test'): - result = requests.get(testenv["starlette_server"] + '/', headers=request_headers) - - self.assertTrue(result) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert asgi_span.sy + assert not test_span.sy + + def test_custom_header_capture(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-Capture-This": "this", + "X-Capture-That": "that", + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - self.assertTrue(test_span.t == urllib3_span.t == asgi_span.t) - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual( result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual( result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertIn("X-Capture-This", asgi_span.data["http"]["header"]) - self.assertEqual("this", asgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", asgi_span.data["http"]["header"]) - self.assertEqual("that", asgi_span.data["http"]["header"]["X-Capture-That"]) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This" in asgi_span.data["http"]["header"] + assert "this" == asgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in asgi_span.data["http"]["header"] + assert "that" == asgi_span.data["http"]["header"]["X-Capture-That"] diff --git a/tests/frameworks/test_starlette_middleware.py b/tests/frameworks/test_starlette_middleware.py new file mode 100644 index 00000000..d02039d4 --- /dev/null +++ b/tests/frameworks/test_starlette_middleware.py @@ -0,0 +1,143 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +from typing import Generator + +import pytest +from instana.singletons import agent, tracer +from starlette.testclient import TestClient + +from tests.apps.starlette_app.app2 import starlette_server +from tests.helpers import get_first_span_by_filter + + +class TestStarletteMiddleware: + """ + Tests Starlette with provided Middleware. + """ + + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from Starlette to make it easier. + self.client = TestClient(starlette_server) + # Clear all spans before a test run. + self.recorder = tracer.span_processor + self.recorder.clear_spans() + yield + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # Starlette instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span + + span_filter = lambda span: span.n == "asgi" # noqa: E731 + asgi_span = get_first_span_by_filter(spans, span_filter) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_basic_get_500(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/five", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span + + span_filter = lambda span: span.n == "asgi" # noqa: E731 + asgi_span = get_first_span_by_filter(spans, span_filter) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["path"] == "/five" + assert asgi_span.data["http"]["path_tpl"] == "/five" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 83242aa0..3d39e2d6 100644 --- a/tests/requirements-310.txt +++ b/tests/requirements-310.txt @@ -40,3 +40,4 @@ sqlalchemy>=2.0.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 diff --git a/tests/requirements-312.txt b/tests/requirements-312.txt index fa87e2ee..cb7fe7c8 100644 --- a/tests/requirements-312.txt +++ b/tests/requirements-312.txt @@ -38,3 +38,4 @@ sqlalchemy>=2.0.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index 0f701231..6d532421 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -51,3 +51,5 @@ sqlalchemy>=2.0.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 +starlette>=0.38.2 diff --git a/tests/requirements-gevent-starlette.txt b/tests/requirements-gevent-starlette.txt index 1333f76c..869b7186 100644 --- a/tests/requirements-gevent-starlette.txt +++ b/tests/requirements-gevent-starlette.txt @@ -7,3 +7,4 @@ pytest>=4.6 starlette>=0.12.13 urllib3>=1.26.5 uvicorn>=0.13.4 +httpx>=0.27.0 diff --git a/tests/requirements.txt b/tests/requirements.txt index 026f94f9..9e8c1165 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -39,3 +39,4 @@ sqlalchemy>=2.0.0 tornado>=4.5.3,<6.0 uvicorn>=0.13.4 urllib3>=1.26.5 +httpx>=0.27.0 From 41c124a1ed2e9ecbddf625c52d4ff00108bd0a5b Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 7 Aug 2024 16:39:15 +0200 Subject: [PATCH 099/172] implementation: added opentelemetry support and related unittests Signed-off-by: Cagri Yonca --- src/instana/instrumentation/pep0249.py | 132 +++++----- tests/clients/test_pep0249.py | 319 +++++++++++++++++++++++++ 2 files changed, 396 insertions(+), 55 deletions(-) create mode 100644 tests/clients/test_pep0249.py diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index d07dc5ef..6129aa60 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -2,105 +2,125 @@ # (c) Copyright Instana Inc. 2018 # This is a wrapper for PEP-0249: Python Database API Specification v2.0 -import opentracing.ext.tags as ext +from __future__ import annotations import wrapt +from typing import TYPE_CHECKING -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off -from ..util.sql import sql_sanitizer +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind + +from instana.log import logger +from instana.util.traceutils import get_tracer_tuple, tracing_is_off +from instana.util.sql import sql_sanitizer + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan class CursorWrapper(wrapt.ObjectProxy): - __slots__ = ('_module_name', '_connect_params', '_cursor_params') + __slots__ = ("_module_name", "_connect_params", "_cursor_params") - def __init__(self, cursor, module_name, - connect_params=None, cursor_params=None): + def __init__( + self, cursor, module_name, connect_params=None, cursor_params=None + ) -> None: super(CursorWrapper, self).__init__(wrapped=cursor) self._module_name = module_name self._connect_params = connect_params self._cursor_params = cursor_params - def _collect_kvs(self, span, sql): + def _collect_kvs(self, span, sql) -> InstanaSpan: try: - span.set_tag(ext.SPAN_KIND, 'exit') - - db_parameter_name = next((p for p in ('db', 'database', 'dbname') if p in self._connect_params[1]), None) + span.set_attribute(SpanKind, "exit") + + db_parameter_name = next( + ( + p + for p in ("db", "database", "dbname") + if p in self._connect_params[1] + ), + None, + ) if db_parameter_name: - span.set_tag(ext.DATABASE_INSTANCE, self._connect_params[1][db_parameter_name]) - - span.set_tag(ext.DATABASE_STATEMENT, sql_sanitizer(sql)) - span.set_tag(ext.DATABASE_USER, self._connect_params[1]['user']) - span.set_tag('host', self._connect_params[1]['host']) - span.set_tag('port', self._connect_params[1]['port']) + span.set_attribute( + SpanAttributes.DB_NAME, + self._connect_params[1][db_parameter_name], + ) + + span.set_attribute(SpanAttributes.DB_STATEMENT, sql_sanitizer(sql)) + span.set_attribute(SpanAttributes.DB_USER, self._connect_params[1]["user"]) + span.set_attribute("host", self._connect_params[1]["host"]) + span.set_attribute("port", self._connect_params[1]["port"]) except Exception as e: logger.debug(e) return span - def __enter__(self): + def __enter__(self) -> CursorWrapper: return self - def execute(self, sql, params=None): - tracer, parent_span, operation_name = get_tracer_tuple() + def execute(self, sql, params=None) -> None: + tracer, _, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through - if (tracing_is_off() or (operation_name == "sqlalchemy")): + if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.execute(sql, params) - with tracer.start_active_span(self._module_name, child_of=parent_span) as scope: + with tracer.start_as_current_span(self._module_name) as span: try: - self._collect_kvs(scope.span, sql) - + self._collect_kvs(span, sql) result = self.__wrapped__.execute(sql, params) except Exception as e: - if scope.span: - scope.span.log_exception(e) + if span: + span.record_exception(e) raise else: return result - def executemany(self, sql, seq_of_parameters): - tracer, parent_span, operation_name = get_tracer_tuple() + def executemany(self, sql, seq_of_parameters) -> None: + tracer, _, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through - if (tracing_is_off() or (operation_name == "sqlalchemy")): + if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.executemany(sql, seq_of_parameters) - with tracer.start_active_span(self._module_name, child_of=parent_span) as scope: + with tracer.start_as_current_span(self._module_name) as span: try: - self._collect_kvs(scope.span, sql) - + self._collect_kvs(span, sql) result = self.__wrapped__.executemany(sql, seq_of_parameters) except Exception as e: - if scope.span: - scope.span.log_exception(e) + if span: + span.record_exception(e) raise else: return result - def callproc(self, proc_name, params): - tracer, parent_span, operation_name = get_tracer_tuple() + def callproc(self, proc_name, params) -> None: + tracer, _, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through - if (tracing_is_off() or (operation_name == "sqlalchemy")): + if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.execute(proc_name, params) - with tracer.start_active_span(self._module_name, child_of=parent_span) as scope: + with tracer.start_as_current_span(self._module_name) as span: try: - self._collect_kvs(scope.span, proc_name) - + self._collect_kvs(span, proc_name) result = self.__wrapped__.callproc(proc_name, params) - except Exception as e: - if scope.span: - scope.span.log_exception(e) - raise + except Exception: + try: + result = self.__wrapped__.execute(proc_name, params) + except Exception as e_execute: + if span: + span.record_exception(e_execute) + raise + else: + return result else: return result class ConnectionWrapper(wrapt.ObjectProxy): - __slots__ = ('_module_name', '_connect_params') + __slots__ = ("_module_name", "_connect_params") - def __init__(self, connection, module_name, connect_params): + def __init__(self, connection, module_name, connect_params) -> None: super(ConnectionWrapper, self).__init__(wrapped=connection) self._module_name = module_name self._connect_params = connect_params @@ -108,33 +128,35 @@ def __init__(self, connection, module_name, connect_params): def __enter__(self): return self - def cursor(self, *args, **kwargs): + def cursor(self, *args, **kwargs) -> CursorWrapper: return CursorWrapper( cursor=self.__wrapped__.cursor(*args, **kwargs), module_name=self._module_name, connect_params=self._connect_params, - cursor_params=(args, kwargs) if args or kwargs else None) + cursor_params=(args, kwargs) if args or kwargs else None, + ) - def begin(self): - return self.__wrapped__.begin() + def close(self) -> None: + return self.__wrapped__.close() - def commit(self): + def commit(self) -> None: return self.__wrapped__.commit() - def rollback(self): + def rollback(self) -> None: return self.__wrapped__.rollback() class ConnectionFactory(object): - def __init__(self, connect_func, module_name): + def __init__(self, connect_func, module_name) -> None: self._connect_func = connect_func self._module_name = module_name self._wrapper_ctor = ConnectionWrapper - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs) -> ConnectionWrapper: connect_params = (args, kwargs) if args or kwargs else None return self._wrapper_ctor( connection=self._connect_func(*args, **kwargs), module_name=self._module_name, - connect_params=connect_params) + connect_params=connect_params, + ) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py new file mode 100644 index 00000000..6d488de2 --- /dev/null +++ b/tests/clients/test_pep0249.py @@ -0,0 +1,319 @@ +import logging +from typing import Generator, TYPE_CHECKING +from unittest.mock import patch +import psycopg2 +import psycopg2.extras +import pytest +from instana.instrumentation.pep0249 import ( + ConnectionFactory, + ConnectionWrapper, + CursorWrapper, +) +from opentelemetry.trace import SpanKind +from instana.singletons import tracer +from instana.util.traceutils import get_tracer_tuple +from pytest import LogCaptureFixture +from instana.span.span import InstanaSpan + + +class TestCursorWrapper: + @pytest.fixture(autouse=True) + def _setup(self) -> Generator[None, None, None]: + self.connect_params = [ + "db", + { + "db": "instana_test_db", + "host": "localhost", + "port": "5432", + "user": "root", + "password": "passw0rd", + }, + ] + self.test_conn = psycopg2.connect( + database=self.connect_params[1]["db"], + host=self.connect_params[1]["host"], + port=self.connect_params[1]["port"], + user=self.connect_params[1]["user"], + password=self.connect_params[1]["password"], + ) + self.cursor_params = {"key": "value"} + self.test_cursor = self.test_conn.cursor() + self.cursor_name = "test-cursor" + self.test_wrapper = CursorWrapper( + self.test_cursor, + self.cursor_name, + self.connect_params, + self.cursor_params, + ) + self.test_cursor.execute( + """ + DROP TABLE IF EXISTS tests; + CREATE TABLE tests (id SERIAL PRIMARY KEY, name VARCHAR(50), email VARCHAR(100)); + """ + ) + self.test_conn.commit() + self.test_cursor.execute( + """ + INSERT INTO tests (id, name, email) VALUES (1, 'test-name', 'testemail@mail.com'); + """ + ) + self.test_conn.commit() + self.test_cursor.execute(""" + CREATE OR REPLACE PROCEDURE insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR) + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO tests (id, name, email) VALUES (test_id, test_name, test_email); + END; + $$; + """) + self.test_conn.commit() + yield + self.test_cursor.close() + self.test_conn.close() + + def test_cursor_wrapper_default(self): + # CursorWrapper + assert self.test_wrapper + assert self.test_wrapper._module_name == self.cursor_name + connection_params = {"db", "host", "port", "user", "password"} + assert connection_params.issubset(self.test_wrapper._connect_params[1].keys()) + assert not self.test_wrapper.closed + assert self.test_wrapper._cursor_params == self.cursor_params + + # Test Connection + assert ( + self.test_conn.dsn + == "user=root password=xxx dbname=instana_test_db host=localhost port=5432" + ) + assert not self.test_conn.autocommit + assert self.test_conn.status == 1 + assert self.test_conn.info.dbname == "instana_test_db" + assert self.test_conn.info.host == "localhost" + assert self.test_conn.info.user == "root" + assert self.test_conn.info.port == 5432 + + # Test Cursor + assert self.test_cursor.arraysize == 1 + assert isinstance(self.test_cursor, psycopg2.extensions.cursor) + assert hasattr(self.test_cursor, "callproc") + assert hasattr(self.test_cursor, "close") + assert hasattr(self.test_cursor, "execute") + assert hasattr(self.test_cursor, "executemany") + assert hasattr(self.test_cursor, "fetchone") + assert hasattr(self.test_cursor, "fetchall") + + def test_collect_kvs(self): + with tracer.start_as_current_span("test") as span: + sample_sql = """ + select * from tests; + """ + self.test_wrapper._collect_kvs(span, sample_sql) + assert span.attributes[SpanKind] == "exit" + assert span.attributes["db.name"] == "instana_test_db" + assert span.attributes["db.statement"] == sample_sql + assert span.attributes["db.user"] == "root" + assert span.attributes["host"] == "localhost" + assert span.attributes["port"] == "5432" + + def test_collect_kvs_error(self, caplog: LogCaptureFixture): + with tracer.start_as_current_span("test") as span: + connect_params = "sample" + sample_wrapper = CursorWrapper( + self.test_cursor, + self.cursor_name, + connect_params, + ) + sample_sql = "select * from tests;" + caplog.set_level(logging.DEBUG, logger="instana") + sample_wrapper._collect_kvs(span, sample_sql) + assert "string indices must be integers" in caplog.messages + + def test_enter(self): + response = self.test_wrapper.__enter__() + assert response == self.test_wrapper + assert isinstance(response, CursorWrapper) + + def test_execute_with_tracing_off(self): + with tracer.start_as_current_span("sqlalchemy"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_params = (2, "sample-name", "sample-email@mail.com") + with patch( + "instana.instrumentation.pep0249.get_tracer_tuple", + wraps=get_tracer_tuple, + ) as mock_get_tracer_tuple: + self.test_wrapper.execute(sample_sql, sample_params) + mock_get_tracer_tuple.assert_called_once() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + def test_execute_with_tracing(self): + with tracer.start_as_current_span("test"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_params = (3, "sample-name", "sample-email@mail.com") + self.test_wrapper.execute(sample_sql, sample_params) + last_inserted_row = self.test_cursor.fetchone() + self.test_conn.commit() + assert last_inserted_row == sample_params + + # Exception Handling + with pytest.raises(Exception) as exc_info, patch.object( + CursorWrapper, "_collect_kvs", side_effect=Exception("test exception") + ) as mock_collect_kvs: + self.test_wrapper.execute(sample_sql) + assert str(exc_info.value) == "test exception" + mock_collect_kvs.assert_called_once() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + def test_executemany_with_tracing_off(self): + with tracer.start_as_current_span("sqlalchemy"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_seq_of_params = [ + (4, "sample-name-3", "sample-email-3@mail.com"), + (5, "sample-name-4", "sample-email-4@mail.com"), + ] + with patch( + "instana.instrumentation.pep0249.get_tracer_tuple", + wraps=get_tracer_tuple, + ) as mocked_object: + self.test_wrapper.executemany(sample_sql, sample_seq_of_params) + mocked_object.assert_called_once() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + for record in sample_seq_of_params: + assert record in response + assert len(response) == 3 + + def test_executemany_with_tracing(self): + with tracer.start_as_current_span("test"): + sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" + sample_seq_of_params = [ + (6, "sample-name-3", "sample-email-3@mail.com"), + (7, "sample-name-4", "sample-email-4@mail.com"), + ] + self.test_wrapper.executemany(sample_sql, sample_seq_of_params) + + # Exception Handling + with pytest.raises(Exception) as exc_info, patch.object( + CursorWrapper, "_collect_kvs", side_effect=Exception("test exception") + ) as mock_collect_kvs: + self.test_wrapper.executemany( + sample_sql, seq_of_parameters=sample_seq_of_params + ) + assert str(exc_info.value) == "test exception" + mock_collect_kvs.assert_called_once() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + for record in sample_seq_of_params: + assert record in response + assert len(response) == 3 + + def test_callproc_with_tracing_off(self): + with tracer.start_as_current_span("sqlalchemy"): + sample_proc_name = "call insert_user(%s, %s, %s);" + sample_params = (8, "sample-name-8", "sample-email-8@mail.com") + self.test_wrapper.callproc(sample_proc_name, sample_params) + self.test_conn.commit() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + def test_callproc_with_tracing(self): + with tracer.start_as_current_span("test"): + sample_proc_name = "call insert_user(%s, %s, %s);" + sample_params = (9, "sample-name-9", "sample-email-9@mail.com") + self.test_wrapper.callproc(sample_proc_name, sample_params) + self.test_conn.commit() + self.test_wrapper.execute("select * from tests;") + response = self.test_wrapper.fetchall() + assert sample_params in response + assert len(response) == 2 + + # Exception Handling + error_proc_name = "erroroeus command;" + with pytest.raises(Exception) as exc_info, patch.object( + InstanaSpan, + "record_exception", + ) as mock_exception: + self.test_wrapper.callproc(error_proc_name, sample_params) + assert exc_info.typename == "SyntaxError" + mock_exception.call_count == 2 + + +class TestConnectionWrapper: + @pytest.fixture(autouse=True) + def _setup(self) -> Generator[None, None, None]: + self.connect_params = [ + "db", + { + "db": "instana_test_db", + "host": "localhost", + "port": "5432", + "user": "root", + "password": "passw0rd", + }, + ] + self.test_conn = psycopg2.connect( + database=self.connect_params[1]["db"], + host=self.connect_params[1]["host"], + port=self.connect_params[1]["port"], + user=self.connect_params[1]["user"], + password=self.connect_params[1]["password"], + ) + self.module_name = "test-connection" + self.connection_manager = ConnectionWrapper( + self.test_conn, self.module_name, self.connect_params + ) + yield + self.test_conn.close() + + def test_enter(self): + response = self.connection_manager.__enter__() + assert isinstance(response, ConnectionWrapper) + assert response._module_name == self.module_name + assert response._connect_params == self.connect_params + + def test_cursor(self): + response = self.connection_manager.cursor() + assert isinstance(response, CursorWrapper) + + def test_close(self): + response = self.connection_manager.close() + assert self.test_conn.closed + assert not response + + def test_commit(self): + response = self.connection_manager.commit() + assert not response + + def test_rollback(self): + if hasattr(self.connection_manager, "rollback"): + response = self.connection_manager.rollback() + assert not response + + +class TestConnectionFactory: + @pytest.fixture(autouse=True) + def _setup(self) -> Generator[None, None, None]: + self.test_conn_func = psycopg2.extras.LogicalReplicationConnection + self.test_module_name = "test-factory" + self.conn_fact = ConnectionFactory(self.test_conn_func, self.test_module_name) + yield + self.test_conn_func = None + self.test_module_name = None + self.conn_fact = None + + def test_call(self): + response = self.conn_fact( + dsn="user=root password=passw0rd dbname=instana_test_db host=localhost port=5432" + ) + assert isinstance(self.conn_fact._wrapper_ctor, ConnectionWrapper.__class__) + assert self.conn_fact._connect_func == self.test_conn_func + assert self.conn_fact._module_name == self.test_module_name + assert isinstance(response, ConnectionWrapper) From 68ae3d3192506c70434831ce551b9251f673e1d2 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 7 Aug 2024 17:03:32 +0200 Subject: [PATCH 100/172] fix: removed procedure if exists Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 6d488de2..bfc4370d 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -59,7 +59,8 @@ def _setup(self) -> Generator[None, None, None]: ) self.test_conn.commit() self.test_cursor.execute(""" - CREATE OR REPLACE PROCEDURE insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR) + DROP PROCEDURE IF EXISTS insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR); + CREATE PROCEDURE insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR) LANGUAGE plpgsql AS $$ BEGIN From 450e659fcbe7583a325f9358dda02e5e7cf352d1 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 7 Aug 2024 17:21:51 +0200 Subject: [PATCH 101/172] fix: updated postgres version used in circle ci Signed-off-by: Cagri Yonca --- .circleci/config.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 98d2e421..8bbe4f55 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -120,7 +120,7 @@ jobs: python38: docker: - image: cimg/python:3.8 - - image: cimg/postgres:9.6.24 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -144,7 +144,7 @@ jobs: python39: docker: - image: cimg/python:3.9 - - image: cimg/postgres:9.6.24 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -168,7 +168,7 @@ jobs: python310: docker: - image: cimg/python:3.10 - - image: cimg/postgres:9.6.24 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -193,7 +193,7 @@ jobs: python311: docker: - image: cimg/python:3.11 - - image: cimg/postgres:9.6.24 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -233,7 +233,7 @@ jobs: python312: docker: - image: cimg/python:3.12 - - image: cimg/postgres:9.6.24 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd @@ -273,7 +273,7 @@ jobs: python313: docker: - image: python:3.13.0rc2-bookworm - - image: cimg/postgres:9.6.24 + - image: cimg/postgres:14.12 environment: POSTGRES_USER: root POSTGRES_PASSWORD: passw0rd From fa59b885cb9d8dae2bf38f6c7c88df35d810119d Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 8 Aug 2024 10:01:30 +0200 Subject: [PATCH 102/172] fix: fixed pytest errors for python 3.10-11-12 Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index bfc4370d..cc0756dd 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -128,7 +128,7 @@ def test_collect_kvs_error(self, caplog: LogCaptureFixture): sample_sql = "select * from tests;" caplog.set_level(logging.DEBUG, logger="instana") sample_wrapper._collect_kvs(span, sample_sql) - assert "string indices must be integers" in caplog.messages + assert "string indices must be integers" in caplog.messages[0] def test_enter(self): response = self.test_wrapper.__enter__() From d070ddf4245c44bae364840b46680d23523fbadd Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 8 Aug 2024 11:53:46 +0200 Subject: [PATCH 103/172] fix: updated conftest.py to ignore pep0249 in python3.13 Signed-off-by: Cagri Yonca --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index 6ba6741c..39ff1d2e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,6 +95,7 @@ # Currently there is a runtime incompatibility caused by the library: # `undefined symbol: _PyInterpreterState_Get` collect_ignore_glob.append("*test_psycopg2*") + collect_ignore_glob.append("*test_pep0249*") collect_ignore_glob.append("*test_sqlalchemy*") # Currently the latest version of pyramid depends on the `cgi` module From 8b2fd40cb5da2e95a4f9ddd5ad5995b711b14168 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 8 Aug 2024 13:46:03 +0200 Subject: [PATCH 104/172] fix: changed parameter from str to int Signed-off-by: Cagri Yonca --- tests/agent/test_host.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/agent/test_host.py b/tests/agent/test_host.py index 032c2215..e27e361e 100644 --- a/tests/agent/test_host.py +++ b/tests/agent/test_host.py @@ -134,11 +134,11 @@ def test_is_agent_listening( mock_response = Mock() mock_response.status_code = 200 with patch.object(requests.Session, "get", return_value=mock_response): - assert agent.is_agent_listening("sample", "1234") + assert agent.is_agent_listening("sample", 1234) mock_response.status_code = 404 with patch.object(requests.Session, "get", return_value=mock_response, clear=True): - assert not agent.is_agent_listening("sample", "1234") + assert not agent.is_agent_listening("sample", 1234) host = "localhost" port = 123 From 052e176a48d0c948d6c5429ea4126cf1eb2f3e01 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 12 Aug 2024 16:27:33 +0200 Subject: [PATCH 105/172] fix: renamed setup function, added span context parameter to tracer Signed-off-by: Cagri Yonca --- src/instana/instrumentation/pep0249.py | 21 +++++++++++++++------ tests/clients/test_pep0249.py | 6 +++--- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index 6129aa60..16025de1 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -58,13 +58,16 @@ def __enter__(self) -> CursorWrapper: return self def execute(self, sql, params=None) -> None: - tracer, _, operation_name = get_tracer_tuple() + tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.execute(sql, params) - with tracer.start_as_current_span(self._module_name) as span: + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: try: self._collect_kvs(span, sql) result = self.__wrapped__.execute(sql, params) @@ -76,13 +79,16 @@ def execute(self, sql, params=None) -> None: return result def executemany(self, sql, seq_of_parameters) -> None: - tracer, _, operation_name = get_tracer_tuple() + tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.executemany(sql, seq_of_parameters) - with tracer.start_as_current_span(self._module_name) as span: + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: try: self._collect_kvs(span, sql) result = self.__wrapped__.executemany(sql, seq_of_parameters) @@ -94,13 +100,16 @@ def executemany(self, sql, seq_of_parameters) -> None: return result def callproc(self, proc_name, params) -> None: - tracer, _, operation_name = get_tracer_tuple() + tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through if tracing_is_off() or (operation_name == "sqlalchemy"): return self.__wrapped__.execute(proc_name, params) - with tracer.start_as_current_span(self._module_name) as span: + parent_context = parent_span.get_span_context() if parent_span else None + with tracer.start_as_current_span( + self._module_name, span_context=parent_context + ) as span: try: self._collect_kvs(span, proc_name) result = self.__wrapped__.callproc(proc_name, params) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index cc0756dd..9c048327 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -18,7 +18,7 @@ class TestCursorWrapper: @pytest.fixture(autouse=True) - def _setup(self) -> Generator[None, None, None]: + def _resource(self) -> Generator[None, None, None]: self.connect_params = [ "db", { @@ -249,7 +249,7 @@ def test_callproc_with_tracing(self): class TestConnectionWrapper: @pytest.fixture(autouse=True) - def _setup(self) -> Generator[None, None, None]: + def _resource(self) -> Generator[None, None, None]: self.connect_params = [ "db", { @@ -301,7 +301,7 @@ def test_rollback(self): class TestConnectionFactory: @pytest.fixture(autouse=True) - def _setup(self) -> Generator[None, None, None]: + def _resource(self) -> Generator[None, None, None]: self.test_conn_func = psycopg2.extras.LogicalReplicationConnection self.test_module_name = "test-factory" self.conn_fact = ConnectionFactory(self.test_conn_func, self.test_module_name) From acc951817aca519679f77c358de5d0dbce98d905 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 13 Aug 2024 10:08:11 +0200 Subject: [PATCH 106/172] fix: changed span kind Signed-off-by: Cagri Yonca --- src/instana/instrumentation/pep0249.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index 16025de1..47f09d07 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -7,7 +7,6 @@ from typing import TYPE_CHECKING from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import SpanKind from instana.log import logger from instana.util.traceutils import get_tracer_tuple, tracing_is_off @@ -30,7 +29,7 @@ def __init__( def _collect_kvs(self, span, sql) -> InstanaSpan: try: - span.set_attribute(SpanKind, "exit") + span.set_attribute("span.kind", "exit") db_parameter_name = next( ( From 70fbbac0837f600697d21305bcc56b0a1df87bc2 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 13 Aug 2024 10:15:21 +0200 Subject: [PATCH 107/172] fix: changed span.kind unittest Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 9c048327..991b4a59 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -110,7 +110,7 @@ def test_collect_kvs(self): select * from tests; """ self.test_wrapper._collect_kvs(span, sample_sql) - assert span.attributes[SpanKind] == "exit" + assert span.attributes["span.kind"] == "exit" assert span.attributes["db.name"] == "instana_test_db" assert span.attributes["db.statement"] == sample_sql assert span.attributes["db.user"] == "root" From 6300c60665fe1da6e8aa684216ee19d8fc62ebdc Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 15 Aug 2024 13:48:51 +0200 Subject: [PATCH 108/172] fix: added type hints and return types Signed-off-by: Cagri Yonca --- src/instana/instrumentation/pep0249.py | 60 ++++++++++++++++++++------ tests/clients/test_pep0249.py | 3 +- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index 47f09d07..49b4ef24 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -2,11 +2,11 @@ # (c) Copyright Instana Inc. 2018 # This is a wrapper for PEP-0249: Python Database API Specification v2.0 -from __future__ import annotations import wrapt -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Any, List, Tuple from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind from instana.log import logger from instana.util.traceutils import get_tracer_tuple, tracing_is_off @@ -20,16 +20,24 @@ class CursorWrapper(wrapt.ObjectProxy): __slots__ = ("_module_name", "_connect_params", "_cursor_params") def __init__( - self, cursor, module_name, connect_params=None, cursor_params=None + self, + cursor: "CursorWrapper", + module_name: str, + connect_params: Dict[str, Any] = None, + cursor_params: Dict[str, Any] = None, ) -> None: super(CursorWrapper, self).__init__(wrapped=cursor) self._module_name = module_name self._connect_params = connect_params self._cursor_params = cursor_params - def _collect_kvs(self, span, sql) -> InstanaSpan: + def _collect_kvs( + self, + span: "InstanaSpan", + sql: str, + ) -> "InstanaSpan": try: - span.set_attribute("span.kind", "exit") + span.set_attribute("span.kind", SpanKind.CLIENT) db_parameter_name = next( ( @@ -53,10 +61,14 @@ def _collect_kvs(self, span, sql) -> InstanaSpan: logger.debug(e) return span - def __enter__(self) -> CursorWrapper: + def __enter__(self) -> "CursorWrapper": return self - def execute(self, sql, params=None) -> None: + def execute( + self, + sql: str, + params: Dict[str, Any] = None, + ) -> None: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -77,7 +89,11 @@ def execute(self, sql, params=None) -> None: else: return result - def executemany(self, sql, seq_of_parameters) -> None: + def executemany( + self, + sql: str, + seq_of_parameters: List[Tuple], + ) -> None: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -98,7 +114,11 @@ def executemany(self, sql, seq_of_parameters) -> None: else: return result - def callproc(self, proc_name, params) -> None: + def callproc( + self, + proc_name: str, + params: Dict[str, Any], + ) -> None: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -128,7 +148,12 @@ def callproc(self, proc_name, params) -> None: class ConnectionWrapper(wrapt.ObjectProxy): __slots__ = ("_module_name", "_connect_params") - def __init__(self, connection, module_name, connect_params) -> None: + def __init__( + self, + connection: "ConnectionWrapper", + module_name: str, + connect_params: Dict[str, Any], + ) -> None: super(ConnectionWrapper, self).__init__(wrapped=connection) self._module_name = module_name self._connect_params = connect_params @@ -136,7 +161,11 @@ def __init__(self, connection, module_name, connect_params) -> None: def __enter__(self): return self - def cursor(self, *args, **kwargs) -> CursorWrapper: + def cursor( + self, + *args: Tuple[int, str, Tuple[Any, ...]], + **kwargs: Dict[str, Any], + ) -> CursorWrapper: return CursorWrapper( cursor=self.__wrapped__.cursor(*args, **kwargs), module_name=self._module_name, @@ -155,14 +184,17 @@ def rollback(self) -> None: class ConnectionFactory(object): - def __init__(self, connect_func, module_name) -> None: + def __init__(self, connect_func: "CursorWrapper", module_name: str) -> None: self._connect_func = connect_func self._module_name = module_name self._wrapper_ctor = ConnectionWrapper - def __call__(self, *args, **kwargs) -> ConnectionWrapper: + def __call__( + self, + *args: Tuple[int, str, Tuple[Any, ...]], + **kwargs: Dict[str, Any], + ) -> ConnectionWrapper: connect_params = (args, kwargs) if args or kwargs else None - return self._wrapper_ctor( connection=self._connect_func(*args, **kwargs), module_name=self._module_name, diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 991b4a59..72ed843d 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -110,7 +110,7 @@ def test_collect_kvs(self): select * from tests; """ self.test_wrapper._collect_kvs(span, sample_sql) - assert span.attributes["span.kind"] == "exit" + assert span.attributes["span.kind"] == SpanKind.CLIENT assert span.attributes["db.name"] == "instana_test_db" assert span.attributes["db.statement"] == sample_sql assert span.attributes["db.user"] == "root" @@ -305,6 +305,7 @@ def _resource(self) -> Generator[None, None, None]: self.test_conn_func = psycopg2.extras.LogicalReplicationConnection self.test_module_name = "test-factory" self.conn_fact = ConnectionFactory(self.test_conn_func, self.test_module_name) + print(type(self.test_conn_func)) yield self.test_conn_func = None self.test_module_name = None From c92c95c7a6605ab7eb4fc4cfde63349d32573532 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 19 Aug 2024 08:49:56 +0200 Subject: [PATCH 109/172] update: updated type annotations Signed-off-by: Cagri Yonca --- src/instana/instrumentation/pep0249.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index 49b4ef24..3b69a4b5 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -3,7 +3,7 @@ # This is a wrapper for PEP-0249: Python Database API Specification v2.0 import wrapt -from typing import TYPE_CHECKING, Dict, Any, List, Tuple +from typing import TYPE_CHECKING, Dict, Any, List, Tuple, Callable from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind @@ -68,7 +68,7 @@ def execute( self, sql: str, params: Dict[str, Any] = None, - ) -> None: + ) -> Callable: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -93,7 +93,7 @@ def executemany( self, sql: str, seq_of_parameters: List[Tuple], - ) -> None: + ) -> Callable: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -118,7 +118,7 @@ def callproc( self, proc_name: str, params: Dict[str, Any], - ) -> None: + ) -> Callable: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -173,18 +173,22 @@ def cursor( cursor_params=(args, kwargs) if args or kwargs else None, ) - def close(self) -> None: + def close(self) -> Callable: return self.__wrapped__.close() - def commit(self) -> None: + def commit(self) -> Callable: return self.__wrapped__.commit() - def rollback(self) -> None: + def rollback(self) -> Callable: return self.__wrapped__.rollback() class ConnectionFactory(object): - def __init__(self, connect_func: "CursorWrapper", module_name: str) -> None: + def __init__( + self, + connect_func: "CursorWrapper", + module_name: str, + ) -> None: self._connect_func = connect_func self._module_name = module_name self._wrapper_ctor = ConnectionWrapper @@ -193,7 +197,7 @@ def __call__( self, *args: Tuple[int, str, Tuple[Any, ...]], **kwargs: Dict[str, Any], - ) -> ConnectionWrapper: + ) -> "ConnectionWrapper": connect_params = (args, kwargs) if args or kwargs else None return self._wrapper_ctor( connection=self._connect_func(*args, **kwargs), From a472ee589c8f42f3bac287fd409bcdc4c3c35a00 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 19 Aug 2024 09:07:47 +0200 Subject: [PATCH 110/172] update: updated setup function Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 72ed843d..087d2ff6 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -45,19 +45,26 @@ def _resource(self) -> Generator[None, None, None]: self.connect_params, self.cursor_params, ) + self.reset_table() + yield + self.test_cursor.close() + self.test_conn.close() + + def reset_table(self): self.test_cursor.execute( """ DROP TABLE IF EXISTS tests; CREATE TABLE tests (id SERIAL PRIMARY KEY, name VARCHAR(50), email VARCHAR(100)); """ ) - self.test_conn.commit() self.test_cursor.execute( """ INSERT INTO tests (id, name, email) VALUES (1, 'test-name', 'testemail@mail.com'); """ ) self.test_conn.commit() + + def reset_procedure(self): self.test_cursor.execute(""" DROP PROCEDURE IF EXISTS insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR); CREATE PROCEDURE insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR) @@ -69,9 +76,6 @@ def _resource(self) -> Generator[None, None, None]: $$; """) self.test_conn.commit() - yield - self.test_cursor.close() - self.test_conn.close() def test_cursor_wrapper_default(self): # CursorWrapper @@ -215,6 +219,7 @@ def test_executemany_with_tracing(self): assert len(response) == 3 def test_callproc_with_tracing_off(self): + self.reset_procedure() with tracer.start_as_current_span("sqlalchemy"): sample_proc_name = "call insert_user(%s, %s, %s);" sample_params = (8, "sample-name-8", "sample-email-8@mail.com") @@ -226,6 +231,7 @@ def test_callproc_with_tracing_off(self): assert len(response) == 2 def test_callproc_with_tracing(self): + self.reset_procedure() with tracer.start_as_current_span("test"): sample_proc_name = "call insert_user(%s, %s, %s);" sample_params = (9, "sample-name-9", "sample-email-9@mail.com") @@ -305,7 +311,6 @@ def _resource(self) -> Generator[None, None, None]: self.test_conn_func = psycopg2.extras.LogicalReplicationConnection self.test_module_name = "test-factory" self.conn_fact = ConnectionFactory(self.test_conn_func, self.test_module_name) - print(type(self.test_conn_func)) yield self.test_conn_func = None self.test_module_name = None From 79414e47fd12fb81a92154484baa3bacafaaad66 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 19 Aug 2024 12:56:12 +0200 Subject: [PATCH 111/172] fix: type annotations has been fixed Signed-off-by: Cagri Yonca --- src/instana/instrumentation/pep0249.py | 42 +++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/instana/instrumentation/pep0249.py b/src/instana/instrumentation/pep0249.py index 3b69a4b5..a6ad5642 100644 --- a/src/instana/instrumentation/pep0249.py +++ b/src/instana/instrumentation/pep0249.py @@ -3,7 +3,8 @@ # This is a wrapper for PEP-0249: Python Database API Specification v2.0 import wrapt -from typing import TYPE_CHECKING, Dict, Any, List, Tuple, Callable +from typing import TYPE_CHECKING, Dict, Any, List, Tuple, Union, Callable, Optional +from typing_extensions import Self from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import SpanKind @@ -21,10 +22,10 @@ class CursorWrapper(wrapt.ObjectProxy): def __init__( self, - cursor: "CursorWrapper", + cursor: Any, module_name: str, - connect_params: Dict[str, Any] = None, - cursor_params: Dict[str, Any] = None, + connect_params: Optional[List[Union[str, Dict[str, Any]]]] = None, + cursor_params: Optional[Dict[str, Any]] = None, ) -> None: super(CursorWrapper, self).__init__(wrapped=cursor) self._module_name = module_name @@ -35,7 +36,7 @@ def _collect_kvs( self, span: "InstanaSpan", sql: str, - ) -> "InstanaSpan": + ) -> None: try: span.set_attribute("span.kind", SpanKind.CLIENT) @@ -59,16 +60,15 @@ def _collect_kvs( span.set_attribute("port", self._connect_params[1]["port"]) except Exception as e: logger.debug(e) - return span - def __enter__(self) -> "CursorWrapper": + def __enter__(self) -> Self: return self def execute( self, sql: str, - params: Dict[str, Any] = None, - ) -> Callable: + params: Optional[Dict[str, Any]] = None, + ) -> Callable[[str, Dict[str, Any]], None]: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -92,8 +92,8 @@ def execute( def executemany( self, sql: str, - seq_of_parameters: List[Tuple], - ) -> Callable: + seq_of_parameters: List[Dict[str, Any]], + ) -> Callable[[str, List[Dict[str, Any]]], None]: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -118,7 +118,7 @@ def callproc( self, proc_name: str, params: Dict[str, Any], - ) -> Callable: + ) -> Callable[[str, Dict[str, Any]], None]: tracer, parent_span, operation_name = get_tracer_tuple() # If not tracing or we're being called from sqlalchemy, just pass through @@ -152,18 +152,18 @@ def __init__( self, connection: "ConnectionWrapper", module_name: str, - connect_params: Dict[str, Any], + connect_params: List[Union[str, Dict[str, Any]]], ) -> None: super(ConnectionWrapper, self).__init__(wrapped=connection) self._module_name = module_name self._connect_params = connect_params - def __enter__(self): + def __enter__(self) -> Self: return self def cursor( self, - *args: Tuple[int, str, Tuple[Any, ...]], + *args: Tuple[int, str, Dict[str, Any]], **kwargs: Dict[str, Any], ) -> CursorWrapper: return CursorWrapper( @@ -173,20 +173,20 @@ def cursor( cursor_params=(args, kwargs) if args or kwargs else None, ) - def close(self) -> Callable: + def close(self) -> Callable[[], None]: return self.__wrapped__.close() - def commit(self) -> Callable: + def commit(self) -> Callable[[], None]: return self.__wrapped__.commit() - def rollback(self) -> Callable: + def rollback(self) -> Callable[[], None]: return self.__wrapped__.rollback() class ConnectionFactory(object): def __init__( self, - connect_func: "CursorWrapper", + connect_func: CursorWrapper, module_name: str, ) -> None: self._connect_func = connect_func @@ -195,9 +195,9 @@ def __init__( def __call__( self, - *args: Tuple[int, str, Tuple[Any, ...]], + *args: Tuple[int, str, Dict[str, Any]], **kwargs: Dict[str, Any], - ) -> "ConnectionWrapper": + ) -> ConnectionWrapper: connect_params = (args, kwargs) if args or kwargs else None return self._wrapper_ctor( connection=self._connect_func(*args, **kwargs), From 09b6269cc28914a44aee42c650463fcce6f40217 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 20 Aug 2024 09:46:06 +0200 Subject: [PATCH 112/172] update: added reset_table and reset_procedure functions Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 087d2ff6..6cebdbae 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -45,7 +45,6 @@ def _resource(self) -> Generator[None, None, None]: self.connect_params, self.cursor_params, ) - self.reset_table() yield self.test_cursor.close() self.test_conn.close() @@ -109,6 +108,7 @@ def test_cursor_wrapper_default(self): assert hasattr(self.test_cursor, "fetchall") def test_collect_kvs(self): + self.reset_table() with tracer.start_as_current_span("test") as span: sample_sql = """ select * from tests; @@ -122,6 +122,7 @@ def test_collect_kvs(self): assert span.attributes["port"] == "5432" def test_collect_kvs_error(self, caplog: LogCaptureFixture): + self.reset_table() with tracer.start_as_current_span("test") as span: connect_params = "sample" sample_wrapper = CursorWrapper( @@ -140,6 +141,7 @@ def test_enter(self): assert isinstance(response, CursorWrapper) def test_execute_with_tracing_off(self): + self.reset_table() with tracer.start_as_current_span("sqlalchemy"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_params = (2, "sample-name", "sample-email@mail.com") @@ -155,6 +157,7 @@ def test_execute_with_tracing_off(self): assert len(response) == 2 def test_execute_with_tracing(self): + self.reset_table() with tracer.start_as_current_span("test"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_params = (3, "sample-name", "sample-email@mail.com") @@ -176,6 +179,7 @@ def test_execute_with_tracing(self): assert len(response) == 2 def test_executemany_with_tracing_off(self): + self.reset_table() with tracer.start_as_current_span("sqlalchemy"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_seq_of_params = [ @@ -195,6 +199,7 @@ def test_executemany_with_tracing_off(self): assert len(response) == 3 def test_executemany_with_tracing(self): + self.reset_table() with tracer.start_as_current_span("test"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_seq_of_params = [ @@ -219,6 +224,7 @@ def test_executemany_with_tracing(self): assert len(response) == 3 def test_callproc_with_tracing_off(self): + self.reset_table() self.reset_procedure() with tracer.start_as_current_span("sqlalchemy"): sample_proc_name = "call insert_user(%s, %s, %s);" @@ -231,6 +237,7 @@ def test_callproc_with_tracing_off(self): assert len(response) == 2 def test_callproc_with_tracing(self): + self.reset_table() self.reset_procedure() with tracer.start_as_current_span("test"): sample_proc_name = "call insert_user(%s, %s, %s);" From 74ede7e9e8355a85bcb0ffc9e24c0afb9afc643a Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 20 Aug 2024 11:23:36 +0200 Subject: [PATCH 113/172] update: updated connection params Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 6cebdbae..ab56f9f2 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -1,6 +1,7 @@ import logging -from typing import Generator, TYPE_CHECKING +from typing import Generator from unittest.mock import patch + import psycopg2 import psycopg2.extras import pytest @@ -9,11 +10,13 @@ ConnectionWrapper, CursorWrapper, ) -from opentelemetry.trace import SpanKind from instana.singletons import tracer +from instana.span.span import InstanaSpan from instana.util.traceutils import get_tracer_tuple +from opentelemetry.trace import SpanKind from pytest import LogCaptureFixture -from instana.span.span import InstanaSpan + +from tests.helpers import testenv class TestCursorWrapper: @@ -22,11 +25,11 @@ def _resource(self) -> Generator[None, None, None]: self.connect_params = [ "db", { - "db": "instana_test_db", - "host": "localhost", - "port": "5432", - "user": "root", - "password": "passw0rd", + "db": testenv["postgresql_db"], + "host": testenv["postgresql_host"], + "port": testenv["postgresql_port"], + "user": testenv["postgresql_user"], + "password": testenv["postgresql_pw"], }, ] self.test_conn = psycopg2.connect( @@ -88,12 +91,12 @@ def test_cursor_wrapper_default(self): # Test Connection assert ( self.test_conn.dsn - == "user=root password=xxx dbname=instana_test_db host=localhost port=5432" + == "user=root password=xxx dbname=instana_test_db host=127.0.0.1 port=5432" ) assert not self.test_conn.autocommit assert self.test_conn.status == 1 assert self.test_conn.info.dbname == "instana_test_db" - assert self.test_conn.info.host == "localhost" + assert self.test_conn.info.host == "127.0.0.1" assert self.test_conn.info.user == "root" assert self.test_conn.info.port == 5432 @@ -118,8 +121,8 @@ def test_collect_kvs(self): assert span.attributes["db.name"] == "instana_test_db" assert span.attributes["db.statement"] == sample_sql assert span.attributes["db.user"] == "root" - assert span.attributes["host"] == "localhost" - assert span.attributes["port"] == "5432" + assert span.attributes["host"] == "127.0.0.1" + assert span.attributes["port"] == 5432 def test_collect_kvs_error(self, caplog: LogCaptureFixture): self.reset_table() @@ -315,7 +318,7 @@ def test_rollback(self): class TestConnectionFactory: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - self.test_conn_func = psycopg2.extras.LogicalReplicationConnection + self.test_conn_func = psycopg2.connect self.test_module_name = "test-factory" self.conn_fact = ConnectionFactory(self.test_conn_func, self.test_module_name) yield From 450b0d6300688c5866a1d4ca352c86c8ec803998 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 22 Aug 2024 08:43:07 +0200 Subject: [PATCH 114/172] fix: removed unnecessary patch block Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index ab56f9f2..0bef6d81 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -148,12 +148,7 @@ def test_execute_with_tracing_off(self): with tracer.start_as_current_span("sqlalchemy"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" sample_params = (2, "sample-name", "sample-email@mail.com") - with patch( - "instana.instrumentation.pep0249.get_tracer_tuple", - wraps=get_tracer_tuple, - ) as mock_get_tracer_tuple: - self.test_wrapper.execute(sample_sql, sample_params) - mock_get_tracer_tuple.assert_called_once() + self.test_wrapper.execute(sample_sql, sample_params) self.test_wrapper.execute("select * from tests;") response = self.test_wrapper.fetchall() assert sample_params in response @@ -189,12 +184,7 @@ def test_executemany_with_tracing_off(self): (4, "sample-name-3", "sample-email-3@mail.com"), (5, "sample-name-4", "sample-email-4@mail.com"), ] - with patch( - "instana.instrumentation.pep0249.get_tracer_tuple", - wraps=get_tracer_tuple, - ) as mocked_object: - self.test_wrapper.executemany(sample_sql, sample_seq_of_params) - mocked_object.assert_called_once() + self.test_wrapper.executemany(sample_sql, sample_seq_of_params) self.test_wrapper.execute("select * from tests;") response = self.test_wrapper.fetchall() for record in sample_seq_of_params: From 8dd13b5af5304df6020d90160613e58cfac39cf3 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 20 Aug 2024 10:04:34 +0530 Subject: [PATCH 115/172] django: refactor instrumentation Signed-off-by: Varsha GS --- src/instana/__init__.py | 2 +- .../instrumentation/django/middleware.py | 81 +++++++++++-------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 4f0310e3..ab82fd54 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -189,7 +189,7 @@ def boot_agent(): # ) # from instana.instrumentation.aws import lambda_inst # noqa: F401 # from instana.instrumentation.celery import hooks # noqa: F401 - # from instana.instrumentation.django import middleware # noqa: F401 + from instana.instrumentation.django import middleware # noqa: F401 # from instana.instrumentation.google.cloud import ( # pubsub, # noqa: F401 # storage, # noqa: F401 diff --git a/src/instana/instrumentation/django/middleware.py b/src/instana/instrumentation/django/middleware.py index d1485163..b3ec6ee4 100644 --- a/src/instana/instrumentation/django/middleware.py +++ b/src/instana/instrumentation/django/middleware.py @@ -2,16 +2,22 @@ # (c) Copyright Instana Inc. 2018 -import os import sys -import opentracing as ot -import opentracing.ext.tags as ext +from opentelemetry import context, trace +from opentelemetry.semconv.trace import SpanAttributes import wrapt +from typing import TYPE_CHECKING, Dict, Any, Callable, Optional, List, Tuple -from ...log import logger -from ...singletons import agent, tracer -from ...util.secrets import strip_secrets_from_query +from instana.log import logger +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query +from instana.propagators.format import Format + +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from django.core.handlers.wsgi import WSGIRequest, WSGIHandler + from django.http import HttpRequest, HttpResponse DJ_INSTANA_MIDDLEWARE = 'instana.instrumentation.django.middleware.InstanaMiddleware' @@ -24,11 +30,11 @@ class InstanaMiddleware(MiddlewareMixin): """ Django Middleware to provide request tracing for Instana """ - def __init__(self, get_response=None): + def __init__(self, get_response: Optional[Callable[["HttpRequest"], "HttpResponse"]]=None) -> None: super(InstanaMiddleware, self).__init__(get_response) self.get_response = get_response - def _extract_custom_headers(self, span, headers, format): + def _extract_custom_headers(self, span: "InstanaSpan", headers: Dict[str, Any], format: bool) -> None: if agent.options.extra_http_headers is None: return @@ -38,37 +44,43 @@ def _extract_custom_headers(self, span, headers, format): django_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if format else custom_header if django_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[django_header]) + span.set_attribute("http.header.%s" % custom_header, headers[django_header]) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - def process_request(self, request): + def process_request(self, request: "WSGIRequest") -> None: try: env = request.environ - ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) - request.iscope = tracer.start_active_span('django', child_of=ctx) + span_context = tracer.extract(Format.HTTP_HEADERS, env) + + span = tracer.start_span("django", span_context=span_context) + request.span = span + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + request.token = token - self._extract_custom_headers(request.iscope.span, env, format=True) + self._extract_custom_headers(span, env, format=True) - request.iscope.span.set_tag(ext.HTTP_METHOD, request.method) + request.span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) if 'PATH_INFO' in env: - request.iscope.span.set_tag(ext.HTTP_URL, env['PATH_INFO']) + request.span.set_attribute(SpanAttributes.HTTP_URL, env['PATH_INFO']) if 'QUERY_STRING' in env and len(env['QUERY_STRING']): scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) - request.iscope.span.set_tag("http.params", scrubbed_params) + request.span.set_attribute("http.params", scrubbed_params) if 'HTTP_HOST' in env: - request.iscope.span.set_tag("http.host", env['HTTP_HOST']) + request.span.set_attribute("http.host", env['HTTP_HOST']) except Exception: logger.debug("Django middleware @ process_request", exc_info=True) - def process_response(self, request, response): + def process_response(self, request: "WSGIRequest", response: "HttpResponse") -> "HttpResponse": try: - if request.iscope is not None: + if request.span: if 500 <= response.status_code: - request.iscope.span.assure_errored() + request.span.assure_errored() # for django >= 2.2 if request.resolver_match is not None and hasattr(request.resolver_match, 'route'): path_tpl = request.resolver_match.route @@ -83,36 +95,37 @@ def process_response(self, request, response): # so the path_tpl is set to None in order not to be added as a tag path_tpl = None if path_tpl: - request.iscope.span.set_tag("http.path_tpl", path_tpl) + request.span.set_attribute("http.path_tpl", path_tpl) - request.iscope.span.set_tag(ext.HTTP_STATUS_CODE, response.status_code) - self._extract_custom_headers(request.iscope.span, response.headers, format=False) - tracer.inject(request.iscope.span.context, ot.Format.HTTP_HEADERS, response) - response['Server-Timing'] = "intid;desc=%s" % request.iscope.span.context.trace_id + request.span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) + self._extract_custom_headers(request.span, response.headers, format=False) + tracer.inject(request.span.context, Format.HTTP_HEADERS, response) + response['Server-Timing'] = "intid;desc=%s" % request.span.context.trace_id except Exception: logger.debug("Instana middleware @ process_response", exc_info=True) finally: - if request.iscope is not None: - request.iscope.close() - request.iscope = None + if request.span: + if request.span.is_recording(): + request.span.end() + request.span = None return response - def process_exception(self, request, exception): + def process_exception(self, request: "WSGIRequest", exception: Exception) -> None: from django.http.response import Http404 if isinstance(exception, Http404): return None - if request.iscope is not None: - request.iscope.span.log_exception(exception) + if request.span: + request.span.record_exception(exception) - def __url_pattern_route(self, view_name): + def __url_pattern_route(self, view_name: str) -> Callable[..., object]: from django.conf import settings from django.urls import RegexURLResolver as URLResolver urlconf = __import__(settings.ROOT_URLCONF, {}, {}, ['']) - def list_urls(urlpatterns, parent_pattern=None): + def list_urls(urlpatterns: List[str], parent_pattern: Optional[List[str]]=None) -> Callable[..., object]: if not urlpatterns: return if parent_pattern is None: @@ -134,7 +147,7 @@ def list_urls(urlpatterns, parent_pattern=None): return list_urls(urlconf.urlpatterns) -def load_middleware_wrapper(wrapped, instance, args, kwargs): +def load_middleware_wrapper(wrapped: Callable[..., None], instance: "WSGIHandler", args: Tuple[object, ...], kwargs: Dict[str, Any]) -> Callable[..., None]: try: from django.conf import settings From 8336051d233425741c60b248e4952d302199945f Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 20 Aug 2024 10:04:58 +0530 Subject: [PATCH 116/172] django: Adapt tests after refactor Signed-off-by: Varsha GS --- tests/apps/app_django.py | 35 +- tests/conftest.py | 1 - tests/frameworks/test_django.py | 602 ++++++++++++++++---------------- 3 files changed, 321 insertions(+), 317 deletions(-) diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index b8a3e58b..796e8d63 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -7,14 +7,16 @@ import os import sys import time -import opentracing -import opentracing.ext.tags as ext + try: from django.urls import re_path except ImportError: from django.conf.urls import url as re_path from django.http import HttpResponse, Http404 +from opentelemetry.semconv.trace import SpanAttributes + +from instana.singletons import tracer filepath, extension = os.path.splitext(__file__) os.environ['DJANGO_SETTINGS_MODULE'] = os.path.basename(filepath) @@ -103,23 +105,22 @@ def not_found(request): def complex(request): - with opentracing.tracer.start_active_span('asteroid') as pscope: - pscope.span.set_tag(ext.COMPONENT, "Python simple example app") - pscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER) - pscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - pscope.span.set_tag(ext.HTTP_URL, "/python/simple/one") - pscope.span.set_tag(ext.HTTP_METHOD, "GET") - pscope.span.set_tag(ext.HTTP_STATUS_CODE, 200) - pscope.span.log_kv({"foo": "bar"}) + with tracer.start_as_current_span("asteroid") as pspan: + pspan.set_attribute("component", "Python simple example app") + pspan.set_attribute("span.kind", "client") + pspan.set_attribute("peer.hostname", "localhost") + pspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/one") + pspan.set_attribute(SpanAttributes.HTTP_METHOD, "GET") + pspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 200) + pspan.add_event(name="complex_request", attributes={"foo": "bar"}) time.sleep(.2) - with opentracing.tracer.start_active_span('spacedust', child_of=pscope.span) as cscope: - cscope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_CLIENT) - cscope.span.set_tag(ext.PEER_HOSTNAME, "localhost") - cscope.span.set_tag(ext.HTTP_URL, "/python/simple/two") - cscope.span.set_tag(ext.HTTP_METHOD, "POST") - cscope.span.set_tag(ext.HTTP_STATUS_CODE, 204) - cscope.span.set_baggage_item("someBaggage", "someValue") + with tracer.start_as_current_span("spacedust") as cspan: + cspan.set_attribute("span.kind", "client") + cspan.set_attribute("peer.hostname", "localhost") + cspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/two") + cspan.set_attribute(SpanAttributes.HTTP_METHOD, "POST") + cspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 204) time.sleep(.1) return HttpResponse('Stan wuz here!') diff --git a/tests/conftest.py b/tests/conftest.py index 39ff1d2e..c6078e5e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,6 @@ collect_ignore_glob.append("*frameworks/test_aiohttp*") collect_ignore_glob.append("*frameworks/test_asyncio*") collect_ignore_glob.append("*frameworks/test_celery*") -collect_ignore_glob.append("*frameworks/test_django*") collect_ignore_glob.append("*frameworks/test_fastapi*") collect_ignore_glob.append("*frameworks/test_gevent*") collect_ignore_glob.append("*frameworks/test_grpcio*") diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index 02778efc..f3123372 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -4,109 +4,113 @@ import os import urllib3 +import pytest +from typing import Generator from django.apps import apps from django.contrib.staticfiles.testing import StaticLiveServerTestCase -from ..apps.app_django import INSTALLED_APPS +from tests.apps.app_django import INSTALLED_APPS from instana.singletons import agent, tracer -from ..helpers import fail_with_message_and_span_dump, get_first_span_by_filter, drop_log_spans_from_list +from tests.helpers import fail_with_message_and_span_dump, get_first_span_by_filter, drop_log_spans_from_list apps.populate(INSTALLED_APPS) class TestDjango(StaticLiveServerTestCase): - def setUp(self): + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: """ Clear all spans before a test run """ - self.recorder = tracer.recorder - self.recorder.clear_spans() self.http = urllib3.PoolManager() + self.recorder = tracer.span_processor + self.recorder.clear_spans() - def tearDown(self): + def tearDown(self) -> None: """ Clear the INSTANA_DISABLE_W3C_TRACE_CORRELATION environment variable """ os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "" - def test_basic_request(self): - with tracer.start_active_span('test'): + def test_basic_request(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/', fields={"test": 1}) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert response.headers['Server-Timing'] == server_timing_value - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertIsNone(django_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert django_span.sy is None + assert urllib3_span.sy is None + assert test_span.sy is None - self.assertEqual(None, django_span.ec) - self.assertEqual('/', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('test=1', django_span.data["http"]["params"]) - self.assertEqual('^$', django_span.data["http"]["path_tpl"]) + assert None == django_span.ec + assert '/' == django_span.data["http"]["url"] + assert 'GET' == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert 'test=1' == django_span.data["http"]["params"] + assert '^$' == django_span.data["http"]["path_tpl"] - self.assertIsNone(django_span.stack) + assert django_span.stack is None - def test_synthetic_request(self): + @pytest.mark.skip("Synthetic is not yet handled") + def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' } - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/', headers=headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertEqual('^$', django_span.data["http"]["path_tpl"]) + assert '^$' == django_span.data["http"]["path_tpl"] - self.assertTrue(django_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert django_span.sy + assert urllib3_span.sy is None + assert test_span.sy is None - def test_request_with_error(self): - with tracer.start_active_span('test'): + def test_request_with_error(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/cause_error') - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert 500 == response.status spans = self.recorder.queued_spans() spans = drop_log_spans_from_list(spans) @@ -118,56 +122,56 @@ def test_request_with_error(self): filter = lambda span: span.n == 'sdk' and span.data['sdk']['name'] == 'test' test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == 'urllib3' urllib3_span = get_first_span_by_filter(spans, filter) - self.assertTrue(urllib3_span) + assert urllib3_span filter = lambda span: span.n == 'django' django_span = get_first_span_by_filter(spans, filter) - self.assertTrue(django_span) + assert django_span - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert response.headers['Server-Timing'] == server_timing_value - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertEqual(1, django_span.ec) + assert 1 == django_span.ec - self.assertEqual('/cause_error', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(500, django_span.data["http"]["status"]) - self.assertEqual('This is a fake error: /cause-error', django_span.data["http"]["error"]) - self.assertEqual('^cause_error$', django_span.data["http"]["path_tpl"]) - self.assertIsNone(django_span.stack) + assert '/cause_error' == django_span.data["http"]["url"] + assert 'GET' == django_span.data["http"]["method"] + assert 500 == django_span.data["http"]["status"] + assert 'This is a fake error: /cause-error' == django_span.data["http"]["error"] + assert '^cause_error$' == django_span.data["http"]["path_tpl"] + assert django_span.stack is None - def test_request_with_not_found(self): - with tracer.start_active_span('test'): + def test_request_with_not_found(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/not_found') - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status spans = self.recorder.queued_spans() spans = drop_log_spans_from_list(spans) @@ -179,17 +183,17 @@ def test_request_with_not_found(self): filter = lambda span: span.n == 'django' django_span = get_first_span_by_filter(spans, filter) - self.assertTrue(django_span) + assert django_span - self.assertIsNone(django_span.ec) - self.assertEqual(404, django_span.data["http"]["status"]) + assert django_span.ec is None + assert 404 == django_span.data["http"]["status"] - def test_request_with_not_found_no_route(self): - with tracer.start_active_span('test'): + def test_request_with_not_found_no_route(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/no_route') - self.assertTrue(response) - self.assertEqual(404, response.status) + assert response + assert 404 == response.status spans = self.recorder.queued_spans() spans = drop_log_spans_from_list(spans) @@ -201,19 +205,19 @@ def test_request_with_not_found_no_route(self): filter = lambda span: span.n == 'django' django_span = get_first_span_by_filter(spans, filter) - self.assertTrue(django_span) - self.assertIsNone(django_span.data["http"]["path_tpl"]) - self.assertIsNone(django_span.ec) - self.assertEqual(404, django_span.data["http"]["status"]) + assert django_span + assert django_span.data["http"]["path_tpl"] is None + assert django_span.ec is None + assert 404 == django_span.data["http"]["status"] - def test_complex_request(self): - with tracer.start_active_span('test'): + def test_complex_request(self) -> None: + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/complex') - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert 5 == len(spans) test_span = spans[4] urllib3_span = spans[3] @@ -221,46 +225,46 @@ def test_complex_request(self): ot_span1 = spans[1] ot_span2 = spans[0] - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) - - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) - self.assertEqual("sdk", ot_span1.n) - self.assertEqual("sdk", ot_span2.n) - - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) - self.assertEqual(django_span.t, ot_span1.t) - self.assertEqual(ot_span1.t, ot_span2.t) - - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) - self.assertEqual(ot_span1.p, django_span.s) - self.assertEqual(ot_span2.p, ot_span1.s) - - self.assertEqual(None, django_span.ec) - self.assertIsNone(django_span.stack) - - self.assertEqual('/complex', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('^complex$', django_span.data["http"]["path_tpl"]) - - def test_request_header_capture(self): + assert response.headers['Server-Timing'] == server_timing_value + + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n + assert "sdk" == ot_span1.n + assert "sdk" == ot_span2.n + + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t + assert django_span.t == ot_span1.t + assert ot_span1.t == ot_span2.t + + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s + assert ot_span1.p == django_span.s + assert ot_span2.p == ot_span1.s + + assert None == django_span.ec + assert django_span.stack is None + + assert '/complex' == django_span.data["http"]["url"] + assert 'GET' == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert '^complex$' == django_span.data["http"]["path_tpl"] + + def test_request_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] @@ -270,89 +274,90 @@ def test_request_header_capture(self): 'X-Capture-That': 'that' } - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) # response = self.client.get('/') - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertEqual(None, django_span.ec) - self.assertIsNone(django_span.stack) + assert None == django_span.ec + assert django_span.stack is None - self.assertEqual('/', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('^$', django_span.data["http"]["path_tpl"]) + assert '/' == django_span.data["http"]["url"] + assert 'GET' == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert '^$' == django_span.data["http"]["path_tpl"] - self.assertIn("X-Capture-This", django_span.data["http"]["header"]) - self.assertEqual("this", django_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", django_span.data["http"]["header"]) - self.assertEqual("that", django_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in django_span.data["http"]["header"] + assert "this" == django_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in django_span.data["http"]["header"] + assert "that" == django_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = [u'X-Capture-This-Too', u'X-Capture-That-Too'] - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.http.request('GET', self.live_server_url + '/response_with_headers') - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert 3 == len(spans) test_span = spans[2] urllib3_span = spans[1] django_span = spans[0] - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual("django", django_span.n) + assert "test" == test_span.data["sdk"]["name"] + assert "urllib3" == urllib3_span.n + assert "django" == django_span.n - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, django_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == django_span.t - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(django_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert django_span.p == urllib3_span.s - self.assertEqual(None, django_span.ec) - self.assertIsNone(django_span.stack) + assert None == django_span.ec + assert django_span.stack is None - self.assertEqual('/response_with_headers', django_span.data["http"]["url"]) - self.assertEqual('GET', django_span.data["http"]["method"]) - self.assertEqual(200, django_span.data["http"]["status"]) - self.assertEqual('^response_with_headers$', django_span.data["http"]["path_tpl"]) + assert '/response_with_headers' == django_span.data["http"]["url"] + assert 'GET' == django_span.data["http"]["method"] + assert 200 == django_span.data["http"]["status"] + assert '^response_with_headers$' == django_span.data["http"]["path_tpl"] - self.assertIn("X-Capture-This-Too", django_span.data["http"]["header"]) - self.assertEqual("this too", django_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", django_span.data["http"]["header"]) - self.assertEqual("that too", django_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-Capture-This-Too" in django_span.data["http"]["header"] + assert "this too" == django_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in django_span.data["http"]["header"] + assert "that too" == django_span.data["http"]["header"]["X-Capture-That-Too"] agent.options.extra_http_headers = original_extra_http_headers - def test_with_incoming_context(self): + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_context(self) -> None: request_headers = dict() request_headers['X-INSTANA-T'] = '1' request_headers['X-INSTANA-S'] = '1' @@ -361,43 +366,44 @@ def test_with_incoming_context(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, '0000000000000001') - self.assertEqual(django_span.p, '0000000000000001') + # assert django_span.t == '0000000000000001' + # assert django_span.p == '0000000000000001' + assert django_span.t == 1 + assert django_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('traceparent', response.headers) + assert 'Server-Timing' in response.headers + server_timing_value = "intid;desc=%s" % django_span.t + assert response.headers['Server-Timing'] == server_timing_value + + assert 'traceparent' in response.headers # The incoming traceparent header had version 01 (which does not exist at the time of writing), but since we # support version 00, we also need to pass down 00 for the version field. - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) + assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s), response.headers['tracestate']) - server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert 'tracestate' in response.headers + assert 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format(django_span.t, django_span.s) == response.headers['tracestate'] - def test_with_incoming_context_and_correlation(self): + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_context_and_correlation(self) -> None: request_headers = dict() request_headers['X-INSTANA-T'] = '1' request_headers['X-INSTANA-S'] = '1' @@ -407,94 +413,92 @@ def test_with_incoming_context_and_correlation(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, 'a3ce929d0e0e4736') - self.assertEqual(django_span.p, '00f067aa0ba902b7') - self.assertEqual(django_span.ia.t, 'a3ce929d0e0e4736') - self.assertEqual(django_span.ia.p, '8357ccd9da194656') - self.assertEqual(django_span.lt, '4bf92f3577b34da6a3ce929d0e0e4736') - self.assertEqual(django_span.tp, True) - self.assertEqual(django_span.crtp, 'web') - self.assertEqual(django_span.crid, '1234567890abcdef') + assert django_span.t == 'a3ce929d0e0e4736' + assert django_span.p == '00f067aa0ba902b7' + assert django_span.ia.t == 'a3ce929d0e0e4736' + assert django_span.ia.p == '8357ccd9da194656' + assert django_span.lt == '4bf92f3577b34da6a3ce929d0e0e4736' + assert django_span.tp == True + assert django_span.crtp == 'web' + assert django_span.crid == '1234567890abcdef' - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('traceparent', response.headers) - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) + assert 'Server-Timing' in response.headers + server_timing_value = "intid;desc=%s" % django_span.t + assert response.headers['Server-Timing'] == server_timing_value - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s), response.headers['tracestate']) + assert 'traceparent' in response.headers + assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert 'tracestate' in response.headers + assert 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( + django_span.t, django_span.s) == response.headers['tracestate'] - def test_with_incoming_traceparent_tracestate(self): + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_traceparent_tracestate(self) -> None: request_headers = dict() request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, 'a3ce929d0e0e4736') # last 16 chars from traceparent trace_id - self.assertEqual(django_span.p, '00f067aa0ba902b7') - self.assertEqual(django_span.ia.t, 'a3ce929d0e0e4736') - self.assertEqual(django_span.ia.p, '8357ccd9da194656') - self.assertEqual(django_span.lt, '4bf92f3577b34da6a3ce929d0e0e4736') - self.assertEqual(django_span.tp, True) + assert django_span.t == 'a3ce929d0e0e4736' # last 16 chars from traceparent trace_id + assert django_span.p == '00f067aa0ba902b7' + assert django_span.ia.t == 'a3ce929d0e0e4736' + assert django_span.ia.p == '8357ccd9da194656' + assert django_span.lt == '4bf92f3577b34da6a3ce929d0e0e4736' + assert django_span.tp == True - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('traceparent', response.headers) - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) + assert 'Server-Timing' in response.headers + server_timing_value = "intid;desc=%s" % django_span.t + assert response.headers['Server-Timing'] == server_timing_value - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.s), response.headers['tracestate']) + assert 'traceparent' in response.headers + assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert 'tracestate' in response.headers + assert 'in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( + django_span.s) == response.headers['tracestate'] - def test_with_incoming_traceparent_tracestate_disable_traceparent(self): + @pytest.mark.skip("Handled when type of trace and span ids are modified to str") + def test_with_incoming_traceparent_tracestate_disable_traceparent(self) -> None: os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "1" request_headers = dict() request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' @@ -502,70 +506,70 @@ def test_with_incoming_traceparent_tracestate_disable_traceparent(self): response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, 'a3ce929d0e0e4736') # last 16 chars from traceparent trace_id - self.assertEqual(django_span.p, '8357ccd9da194656') + assert django_span.t == 'a3ce929d0e0e4736' # last 16 chars from traceparent trace_id + assert django_span.p == '8357ccd9da194656' - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' - self.assertIn('traceparent', response.headers) - self.assertEqual('00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s), - response.headers['traceparent']) + assert 'Server-Timing' in response.headers + server_timing_value = "intid;desc=%s" % django_span.t + assert response.headers['Server-Timing'] == server_timing_value - self.assertIn('tracestate', response.headers) - self.assertEqual( - 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s), response.headers['tracestate']) + assert 'traceparent' in response.headers + assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert 'tracestate' in response.headers + assert 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( + django_span.t, django_span.s) == response.headers['tracestate'] - def test_with_incoming_mixed_case_context(self): + def test_with_incoming_mixed_case_context(self) -> None: request_headers = dict() request_headers['X-InSTANa-T'] = '0000000000000001' request_headers['X-instana-S'] = '0000000000000001' response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert 200 == response.status spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) django_span = spans[0] - self.assertEqual(django_span.t, '0000000000000001') - self.assertEqual(django_span.p, '0000000000000001') + # assert django_span.t == '0000000000000001' + # assert django_span.p == '0000000000000001' + assert django_span.t == 1 + assert django_span.p == 1 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(django_span.t, response.headers['X-INSTANA-T']) + assert 'X-INSTANA-T' in response.headers + assert int(response.headers['X-INSTANA-T'], 16) + assert response.headers["X-INSTANA-T"] == str(django_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(django_span.s, response.headers['X-INSTANA-S']) + assert 'X-INSTANA-S' in response.headers + assert int(response.headers['X-INSTANA-S'], 16) + assert response.headers["X-INSTANA-S"] == str(django_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual('1', response.headers['X-INSTANA-L']) + assert 'X-INSTANA-L' in response.headers + assert response.headers['X-INSTANA-L'] == '1' + assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % django_span.t - self.assertIn('Server-Timing', response.headers) - self.assertEqual(server_timing_value, response.headers['Server-Timing']) + assert response.headers['Server-Timing'] == server_timing_value From f485f519f5c486450c4c7d9fda7cb7ac93a5be7e Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 22 Aug 2024 20:33:46 +0530 Subject: [PATCH 117/172] tests(django): increase coverage Signed-off-by: Varsha GS --- .../instrumentation/django/middleware.py | 60 ++++++++++--------- tests/apps/app_django.py | 13 +++- tests/frameworks/test_django.py | 22 +++++-- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/instana/instrumentation/django/middleware.py b/src/instana/instrumentation/django/middleware.py index b3ec6ee4..4f989f74 100644 --- a/src/instana/instrumentation/django/middleware.py +++ b/src/instana/instrumentation/django/middleware.py @@ -89,7 +89,7 @@ def process_response(self, request: "WSGIRequest", response: "HttpResponse") -> try: from django.urls import resolve view_name = resolve(request.path)._func_path - path_tpl = "".join(self.__url_pattern_route(view_name)) + path_tpl = "".join(url_pattern_route(view_name)) except Exception: # the resolve method can fire a Resolver404 exception, in this case there is no matching route # so the path_tpl is set to None in order not to be added as a tag @@ -108,6 +108,9 @@ def process_response(self, request: "WSGIRequest", response: "HttpResponse") -> if request.span.is_recording(): request.span.end() request.span = None + if request.token: + context.detach(request.token) + request.token = None return response def process_exception(self, request: "WSGIRequest", exception: Exception) -> None: @@ -119,32 +122,35 @@ def process_exception(self, request: "WSGIRequest", exception: Exception) -> Non if request.span: request.span.record_exception(exception) - def __url_pattern_route(self, view_name: str) -> Callable[..., object]: - from django.conf import settings - from django.urls import RegexURLResolver as URLResolver - - urlconf = __import__(settings.ROOT_URLCONF, {}, {}, ['']) - - def list_urls(urlpatterns: List[str], parent_pattern: Optional[List[str]]=None) -> Callable[..., object]: - if not urlpatterns: - return - if parent_pattern is None: - parent_pattern = [] - first = urlpatterns[0] - if isinstance(first, URLPattern): - if first.lookup_str == view_name: - if hasattr(first, "regex"): - return parent_pattern + [str(first.regex.pattern)] - else: - return parent_pattern + [str(first.pattern)] - elif isinstance(first, URLResolver): +def url_pattern_route(view_name: str) -> Callable[..., object]: + from django.conf import settings + try: + from django.urls import (RegexURLPattern as URLPattern, RegexURLResolver as URLResolver) + except ImportError: + from django.urls import URLPattern, URLResolver + + urlconf = __import__(settings.ROOT_URLCONF, {}, {}, ['']) + + def list_urls(urlpatterns: List[str], parent_pattern: Optional[List[str]]=None) -> Callable[..., object]: + if not urlpatterns: + return + if parent_pattern is None: + parent_pattern = [] + first = urlpatterns[0] + if isinstance(first, URLPattern): + if first.lookup_str == view_name: if hasattr(first, "regex"): - return list_urls(first.url_patterns, parent_pattern + [str(first.regex.pattern)]) + return parent_pattern + [str(first.regex.pattern)] else: - return list_urls(first.url_patterns, parent_pattern + [str(first.pattern)]) - return list_urls(urlpatterns[1:], parent_pattern) + return parent_pattern + [str(first.pattern)] + elif isinstance(first, URLResolver): + if hasattr(first, "regex"): + return list_urls(first.url_patterns, parent_pattern + [str(first.regex.pattern)]) + else: + return list_urls(first.url_patterns, parent_pattern + [str(first.pattern)]) + return list_urls(urlpatterns[1:], parent_pattern) - return list_urls(urlconf.urlpatterns) + return list_urls(urlconf.urlpatterns) def load_middleware_wrapper(wrapped: Callable[..., None], instance: "WSGIHandler", args: Tuple[object, ...], kwargs: Dict[str, Any]) -> Callable[..., None]: @@ -164,7 +170,7 @@ def load_middleware_wrapper(wrapped: Callable[..., None], instance: "WSGIHandler else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") - elif hasattr(settings, 'MIDDLEWARE_CLASSES') and settings.MIDDLEWARE_CLASSES is not None: + elif hasattr(settings, 'MIDDLEWARE_CLASSES') and settings.MIDDLEWARE_CLASSES is not None: # pragma: no cover if DJ_INSTANA_MIDDLEWARE in settings.MIDDLEWARE_CLASSES: return wrapped(*args, **kwargs) @@ -175,7 +181,7 @@ def load_middleware_wrapper(wrapped: Callable[..., None], instance: "WSGIHandler else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") - else: + else: # pragma: no cover logger.warning("Instana: Couldn't find middleware settings") return wrapped(*args, **kwargs) @@ -188,7 +194,7 @@ def load_middleware_wrapper(wrapped: Callable[..., None], instance: "WSGIHandler logger.debug("Instrumenting django") wrapt.wrap_function_wrapper('django.core.handlers.base', 'BaseHandler.load_middleware', load_middleware_wrapper) - if '/tmp/.instana/python' in sys.path: + if '/tmp/.instana/python' in sys.path: # pragma: no cover # If we are instrumenting via AutoTrace (in an already running process), then the # WSGI middleware has to be live reloaded. from django.core.servers.basehttp import get_internal_wsgi_application diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index 796e8d63..8ea3a306 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -9,7 +9,7 @@ import time try: - from django.urls import re_path + from django.urls import re_path, include except ImportError: from django.conf.urls import url as re_path @@ -96,6 +96,10 @@ def cause_error(request): raise Exception('This is a fake error: /cause-error') +def induce_exception(request): + raise Exception('This is a fake error: /induce-exception') + + def another(request): return HttpResponse('Stan wuz here!') @@ -134,11 +138,16 @@ def response_with_headers(request): return HttpResponse('Stan wuz here with headers!', headers=headers) +extra_patterns = [ + re_path(r'^induce_exception$', induce_exception, name='induce_exception'), +] + urlpatterns = [ re_path(r'^$', index, name='index'), re_path(r'^cause_error$', cause_error, name='cause_error'), re_path(r'^another$', another), re_path(r'^not_found$', not_found, name='not_found'), + re_path(r'^response_with_headers$', response_with_headers, name='response_with_headers'), + re_path(r"^exception$", include(extra_patterns)), re_path(r'^complex$', complex, name='complex'), - re_path(r'^response_with_headers$', response_with_headers, name='response_with_headers') ] diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index f3123372..26413ffa 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -12,6 +12,7 @@ from tests.apps.app_django import INSTALLED_APPS from instana.singletons import agent, tracer from tests.helpers import fail_with_message_and_span_dump, get_first_span_by_filter, drop_log_spans_from_list +from instana.instrumentation.django.middleware import url_pattern_route apps.populate(INSTALLED_APPS) @@ -19,13 +20,13 @@ class TestDjango(StaticLiveServerTestCase): @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Clear all spans before a test run """ + """ Setup and Teardown """ self.http = urllib3.PoolManager() self.recorder = tracer.span_processor + # clear all spans before a test run self.recorder.clear_spans() - - def tearDown(self) -> None: - """ Clear the INSTANA_DISABLE_W3C_TRACE_CORRELATION environment variable """ + yield + # clear the INSTANA_DISABLE_W3C_TRACE_CORRELATION environment variable os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "" def test_basic_request(self) -> None: @@ -80,7 +81,6 @@ def test_basic_request(self) -> None: assert django_span.stack is None - @pytest.mark.skip("Synthetic is not yet handled") def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' @@ -573,3 +573,15 @@ def test_with_incoming_mixed_case_context(self) -> None: assert 'Server-Timing' in response.headers server_timing_value = "intid;desc=%s" % django_span.t assert response.headers['Server-Timing'] == server_timing_value + + def test_url_pattern_route(self) -> None: + view_name="app_django.another" + path_tpl = "".join(url_pattern_route(view_name)) + assert path_tpl == "^another$" + + view_name="app_django.complex" + try: + path_tpl = "".join(url_pattern_route(view_name)) + except Exception: + path_tpl = None + assert path_tpl is None From 1c30c8c157a51735c0dea0c6d8818db60b23dfc5 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 23 Aug 2024 14:38:20 +0530 Subject: [PATCH 118/172] sdk_span: Add OTel SpanKind to entry and exit kind Signed-off-by: Varsha GS --- src/instana/span/kind.py | 8 +++++--- tests/apps/app_django.py | 5 +++-- tests/frameworks/test_django.py | 29 +++++++++++++++++------------ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/instana/span/kind.py b/src/instana/span/kind.py index 8b8c6ea7..263018fb 100644 --- a/src/instana/span/kind.py +++ b/src/instana/span/kind.py @@ -1,10 +1,12 @@ # (c) Copyright IBM Corp. 2024 -ENTRY_KIND = ("entry", "server", "consumer") +from opentelemetry.trace import SpanKind -EXIT_KIND = ("exit", "client", "producer") +ENTRY_KIND = ("entry", "server", "consumer", SpanKind.SERVER, SpanKind.CONSUMER) -LOCAL_SPANS = ("render",) +EXIT_KIND = ("exit", "client", "producer", SpanKind.CLIENT, SpanKind.PRODUCER) + +LOCAL_SPANS = ("render", SpanKind.INTERNAL) HTTP_SPANS = ( "aiohttp-client", diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index 8ea3a306..538c4f83 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -15,6 +15,7 @@ from django.http import HttpResponse, Http404 from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind from instana.singletons import tracer @@ -111,7 +112,7 @@ def not_found(request): def complex(request): with tracer.start_as_current_span("asteroid") as pspan: pspan.set_attribute("component", "Python simple example app") - pspan.set_attribute("span.kind", "client") + pspan.set_attribute("span.kind", SpanKind.CLIENT) pspan.set_attribute("peer.hostname", "localhost") pspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/one") pspan.set_attribute(SpanAttributes.HTTP_METHOD, "GET") @@ -120,7 +121,7 @@ def complex(request): time.sleep(.2) with tracer.start_as_current_span("spacedust") as cspan: - cspan.set_attribute("span.kind", "client") + cspan.set_attribute("span.kind", SpanKind.CLIENT) cspan.set_attribute("peer.hostname", "localhost") cspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/two") cspan.set_attribute(SpanAttributes.HTTP_METHOD, "POST") diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index 26413ffa..15597f3d 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -72,7 +72,7 @@ def test_basic_request(self) -> None: assert urllib3_span.sy is None assert test_span.sy is None - assert None == django_span.ec + assert django_span.ec is None assert '/' == django_span.data["http"]["url"] assert 'GET' == django_span.data["http"]["method"] assert 200 == django_span.data["http"]["status"] @@ -222,8 +222,8 @@ def test_complex_request(self) -> None: test_span = spans[4] urllib3_span = spans[3] django_span = spans[2] - ot_span1 = spans[1] - ot_span2 = spans[0] + otel_span1 = spans[1] + otel_span2 = spans[0] assert 'X-INSTANA-T' in response.headers assert int(response.headers['X-INSTANA-T'], 16) @@ -243,22 +243,27 @@ def test_complex_request(self) -> None: assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n assert "django" == django_span.n - assert "sdk" == ot_span1.n - assert "sdk" == ot_span2.n + assert "sdk" == otel_span1.n + assert "sdk" == otel_span2.n assert test_span.t == urllib3_span.t assert urllib3_span.t == django_span.t - assert django_span.t == ot_span1.t - assert ot_span1.t == ot_span2.t + assert django_span.t == otel_span1.t + assert otel_span1.t == otel_span2.t assert urllib3_span.p == test_span.s assert django_span.p == urllib3_span.s - assert ot_span1.p == django_span.s - assert ot_span2.p == ot_span1.s + assert otel_span1.p == django_span.s + assert otel_span2.p == otel_span1.s - assert None == django_span.ec + assert django_span.ec is None assert django_span.stack is None + assert otel_span1.data["sdk"]["type"] == "exit" + assert otel_span2.data["sdk"]["type"] == otel_span1.data["sdk"]["type"] + otel_span1.data["sdk"]["name"] == "asteroid" + otel_span2.data["sdk"]["name"] == "spacedust" + assert '/complex' == django_span.data["http"]["url"] assert 'GET' == django_span.data["http"]["method"] assert 200 == django_span.data["http"]["status"] @@ -298,7 +303,7 @@ def test_request_header_capture(self) -> None: assert urllib3_span.p == test_span.s assert django_span.p == urllib3_span.s - assert None == django_span.ec + assert django_span.ec is None assert django_span.stack is None assert '/' == django_span.data["http"]["url"] @@ -341,7 +346,7 @@ def test_response_header_capture(self) -> None: assert urllib3_span.p == test_span.s assert django_span.p == urllib3_span.s - assert None == django_span.ec + assert django_span.ec is None assert django_span.stack is None assert '/response_with_headers' == django_span.data["http"]["url"] From 3f7b8e53c27cbb3a69c6f43337fef28cdebd8054 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 23 Aug 2024 14:43:21 +0530 Subject: [PATCH 119/172] style(django): Add style changes Signed-off-by: Varsha GS --- .../instrumentation/django/middleware.py | 121 +++-- tests/apps/app_django.py | 113 +++-- tests/frameworks/test_django.py | 418 ++++++++++-------- 3 files changed, 386 insertions(+), 266 deletions(-) diff --git a/src/instana/instrumentation/django/middleware.py b/src/instana/instrumentation/django/middleware.py index 4f989f74..82ef5ed5 100644 --- a/src/instana/instrumentation/django/middleware.py +++ b/src/instana/instrumentation/django/middleware.py @@ -19,7 +19,7 @@ from django.core.handlers.wsgi import WSGIRequest, WSGIHandler from django.http import HttpRequest, HttpResponse -DJ_INSTANA_MIDDLEWARE = 'instana.instrumentation.django.middleware.InstanaMiddleware' +DJ_INSTANA_MIDDLEWARE = "instana.instrumentation.django.middleware.InstanaMiddleware" try: from django.utils.deprecation import MiddlewareMixin @@ -28,23 +28,33 @@ class InstanaMiddleware(MiddlewareMixin): - """ Django Middleware to provide request tracing for Instana """ + """Django Middleware to provide request tracing for Instana""" - def __init__(self, get_response: Optional[Callable[["HttpRequest"], "HttpResponse"]]=None) -> None: + def __init__( + self, get_response: Optional[Callable[["HttpRequest"], "HttpResponse"]] = None + ) -> None: super(InstanaMiddleware, self).__init__(get_response) self.get_response = get_response - def _extract_custom_headers(self, span: "InstanaSpan", headers: Dict[str, Any], format: bool) -> None: + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any], format: bool + ) -> None: if agent.options.extra_http_headers is None: return - try: + try: for custom_header in agent.options.extra_http_headers: # Headers are available in this format: HTTP_X_CAPTURE_THIS - django_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if format else custom_header + django_header = ( + ("HTTP_" + custom_header.upper()).replace("-", "_") + if format + else custom_header + ) if django_header in headers: - span.set_attribute("http.header.%s" % custom_header, headers[django_header]) + span.set_attribute( + "http.header.%s" % custom_header, headers[django_header] + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) @@ -65,29 +75,37 @@ def process_request(self, request: "WSGIRequest") -> None: self._extract_custom_headers(span, env, format=True) request.span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) - if 'PATH_INFO' in env: - request.span.set_attribute(SpanAttributes.HTTP_URL, env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, - agent.options.secrets_list) + if "PATH_INFO" in env: + request.span.set_attribute(SpanAttributes.HTTP_URL, env["PATH_INFO"]) + if "QUERY_STRING" in env and len(env["QUERY_STRING"]): + scrubbed_params = strip_secrets_from_query( + env["QUERY_STRING"], + agent.options.secrets_matcher, + agent.options.secrets_list, + ) request.span.set_attribute("http.params", scrubbed_params) - if 'HTTP_HOST' in env: - request.span.set_attribute("http.host", env['HTTP_HOST']) + if "HTTP_HOST" in env: + request.span.set_attribute("http.host", env["HTTP_HOST"]) except Exception: logger.debug("Django middleware @ process_request", exc_info=True) - def process_response(self, request: "WSGIRequest", response: "HttpResponse") -> "HttpResponse": + def process_response( + self, request: "WSGIRequest", response: "HttpResponse" + ) -> "HttpResponse": try: if request.span: if 500 <= response.status_code: request.span.assure_errored() # for django >= 2.2 - if request.resolver_match is not None and hasattr(request.resolver_match, 'route'): + if request.resolver_match is not None and hasattr( + request.resolver_match, "route" + ): path_tpl = request.resolver_match.route # django < 2.2 or in case of 404 else: try: from django.urls import resolve + view_name = resolve(request.path)._func_path path_tpl = "".join(url_pattern_route(view_name)) except Exception: @@ -97,10 +115,16 @@ def process_response(self, request: "WSGIRequest", response: "HttpResponse") -> if path_tpl: request.span.set_attribute("http.path_tpl", path_tpl) - request.span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status_code) - self._extract_custom_headers(request.span, response.headers, format=False) + request.span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, response.status_code + ) + self._extract_custom_headers( + request.span, response.headers, format=False + ) tracer.inject(request.span.context, Format.HTTP_HEADERS, response) - response['Server-Timing'] = "intid;desc=%s" % request.span.context.trace_id + response["Server-Timing"] = ( + "intid;desc=%s" % request.span.context.trace_id + ) except Exception: logger.debug("Instana middleware @ process_response", exc_info=True) finally: @@ -122,16 +146,23 @@ def process_exception(self, request: "WSGIRequest", exception: Exception) -> Non if request.span: request.span.record_exception(exception) + def url_pattern_route(view_name: str) -> Callable[..., object]: from django.conf import settings + try: - from django.urls import (RegexURLPattern as URLPattern, RegexURLResolver as URLResolver) + from django.urls import ( + RegexURLPattern as URLPattern, + RegexURLResolver as URLResolver, + ) except ImportError: from django.urls import URLPattern, URLResolver - urlconf = __import__(settings.ROOT_URLCONF, {}, {}, ['']) + urlconf = __import__(settings.ROOT_URLCONF, {}, {}, [""]) - def list_urls(urlpatterns: List[str], parent_pattern: Optional[List[str]]=None) -> Callable[..., object]: + def list_urls( + urlpatterns: List[str], parent_pattern: Optional[List[str]] = None + ) -> Callable[..., object]: if not urlpatterns: return if parent_pattern is None: @@ -145,21 +176,30 @@ def list_urls(urlpatterns: List[str], parent_pattern: Optional[List[str]]=None) return parent_pattern + [str(first.pattern)] elif isinstance(first, URLResolver): if hasattr(first, "regex"): - return list_urls(first.url_patterns, parent_pattern + [str(first.regex.pattern)]) + return list_urls( + first.url_patterns, parent_pattern + [str(first.regex.pattern)] + ) else: - return list_urls(first.url_patterns, parent_pattern + [str(first.pattern)]) + return list_urls( + first.url_patterns, parent_pattern + [str(first.pattern)] + ) return list_urls(urlpatterns[1:], parent_pattern) return list_urls(urlconf.urlpatterns) -def load_middleware_wrapper(wrapped: Callable[..., None], instance: "WSGIHandler", args: Tuple[object, ...], kwargs: Dict[str, Any]) -> Callable[..., None]: +def load_middleware_wrapper( + wrapped: Callable[..., None], + instance: "WSGIHandler", + args: Tuple[object, ...], + kwargs: Dict[str, Any], +) -> Callable[..., None]: try: from django.conf import settings # Django >=1.10 to <2.0 support old-style MIDDLEWARE_CLASSES so we # do as well here - if hasattr(settings, 'MIDDLEWARE') and settings.MIDDLEWARE is not None: + if hasattr(settings, "MIDDLEWARE") and settings.MIDDLEWARE is not None: if DJ_INSTANA_MIDDLEWARE in settings.MIDDLEWARE: return wrapped(*args, **kwargs) @@ -170,31 +210,44 @@ def load_middleware_wrapper(wrapped: Callable[..., None], instance: "WSGIHandler else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") - elif hasattr(settings, 'MIDDLEWARE_CLASSES') and settings.MIDDLEWARE_CLASSES is not None: # pragma: no cover + elif ( + hasattr(settings, "MIDDLEWARE_CLASSES") + and settings.MIDDLEWARE_CLASSES is not None + ): # pragma: no cover if DJ_INSTANA_MIDDLEWARE in settings.MIDDLEWARE_CLASSES: return wrapped(*args, **kwargs) if isinstance(settings.MIDDLEWARE_CLASSES, tuple): - settings.MIDDLEWARE_CLASSES = (DJ_INSTANA_MIDDLEWARE,) + settings.MIDDLEWARE_CLASSES + settings.MIDDLEWARE_CLASSES = ( + DJ_INSTANA_MIDDLEWARE, + ) + settings.MIDDLEWARE_CLASSES elif isinstance(settings.MIDDLEWARE_CLASSES, list): - settings.MIDDLEWARE_CLASSES = [DJ_INSTANA_MIDDLEWARE] + settings.MIDDLEWARE_CLASSES + settings.MIDDLEWARE_CLASSES = [ + DJ_INSTANA_MIDDLEWARE + ] + settings.MIDDLEWARE_CLASSES else: logger.warning("Instana: Couldn't add InstanaMiddleware to Django") - else: # pragma: no cover + else: # pragma: no cover logger.warning("Instana: Couldn't find middleware settings") return wrapped(*args, **kwargs) except Exception: - logger.warning("Instana: Couldn't add InstanaMiddleware to Django: ", exc_info=True) + logger.warning( + "Instana: Couldn't add InstanaMiddleware to Django: ", exc_info=True + ) try: - if 'django' in sys.modules: + if "django" in sys.modules: logger.debug("Instrumenting django") - wrapt.wrap_function_wrapper('django.core.handlers.base', 'BaseHandler.load_middleware', load_middleware_wrapper) + wrapt.wrap_function_wrapper( + "django.core.handlers.base", + "BaseHandler.load_middleware", + load_middleware_wrapper, + ) - if '/tmp/.instana/python' in sys.path: # pragma: no cover + if "/tmp/.instana/python" in sys.path: # pragma: no cover # If we are instrumenting via AutoTrace (in an already running process), then the # WSGI middleware has to be live reloaded. from django.core.servers.basehttp import get_internal_wsgi_application diff --git a/tests/apps/app_django.py b/tests/apps/app_django.py index 538c4f83..5e7227ac 100755 --- a/tests/apps/app_django.py +++ b/tests/apps/app_django.py @@ -20,93 +20,93 @@ from instana.singletons import tracer filepath, extension = os.path.splitext(__file__) -os.environ['DJANGO_SETTINGS_MODULE'] = os.path.basename(filepath) +os.environ["DJANGO_SETTINGS_MODULE"] = os.path.basename(filepath) sys.path.insert(0, os.path.dirname(os.path.abspath(filepath))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SECRET_KEY = '^(myu#*^5v-9o$i-%6vnlwvy^#7&hspj$m3lcq#b$@__@+zd@c' +SECRET_KEY = "^(myu#*^5v-9o$i-%6vnlwvy^#7&hspj$m3lcq#b$@__@+zd@c" DEBUG = True -ALLOWED_HOSTS = ['testserver', 'localhost'] +ALLOWED_HOSTS = ["testserver", "localhost"] INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'app_django' +ROOT_URLCONF = "app_django" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] -WSGI_APPLICATION = 'app_django.wsgi.application' +WSGI_APPLICATION = "app_django.wsgi.application" DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True -STATIC_URL = '/static/' +STATIC_URL = "/static/" def index(request): - return HttpResponse('Stan wuz here!') + return HttpResponse("Stan wuz here!") def cause_error(request): - raise Exception('This is a fake error: /cause-error') + raise Exception("This is a fake error: /cause-error") def induce_exception(request): - raise Exception('This is a fake error: /induce-exception') + raise Exception("This is a fake error: /induce-exception") def another(request): - return HttpResponse('Stan wuz here!') + return HttpResponse("Stan wuz here!") def not_found(request): - raise Http404('Nothing here') + raise Http404("Nothing here") def complex(request): @@ -118,7 +118,7 @@ def complex(request): pspan.set_attribute(SpanAttributes.HTTP_METHOD, "GET") pspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 200) pspan.add_event(name="complex_request", attributes={"foo": "bar"}) - time.sleep(.2) + time.sleep(0.2) with tracer.start_as_current_span("spacedust") as cspan: cspan.set_attribute("span.kind", SpanKind.CLIENT) @@ -126,29 +126,28 @@ def complex(request): cspan.set_attribute(SpanAttributes.HTTP_URL, "/python/simple/two") cspan.set_attribute(SpanAttributes.HTTP_METHOD, "POST") cspan.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 204) - time.sleep(.1) + time.sleep(0.1) - return HttpResponse('Stan wuz here!') + return HttpResponse("Stan wuz here!") def response_with_headers(request): - headers = { - 'X-Capture-This-Too': 'this too', - 'X-Capture-That-Too': 'that too' - } - return HttpResponse('Stan wuz here with headers!', headers=headers) + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} + return HttpResponse("Stan wuz here with headers!", headers=headers) extra_patterns = [ - re_path(r'^induce_exception$', induce_exception, name='induce_exception'), + re_path(r"^induce_exception$", induce_exception, name="induce_exception"), ] urlpatterns = [ - re_path(r'^$', index, name='index'), - re_path(r'^cause_error$', cause_error, name='cause_error'), - re_path(r'^another$', another), - re_path(r'^not_found$', not_found, name='not_found'), - re_path(r'^response_with_headers$', response_with_headers, name='response_with_headers'), + re_path(r"^$", index, name="index"), + re_path(r"^cause_error$", cause_error, name="cause_error"), + re_path(r"^another$", another), + re_path(r"^not_found$", not_found, name="not_found"), + re_path( + r"^response_with_headers$", response_with_headers, name="response_with_headers" + ), re_path(r"^exception$", include(extra_patterns)), - re_path(r'^complex$', complex, name='complex'), + re_path(r"^complex$", complex, name="complex"), ] diff --git a/tests/frameworks/test_django.py b/tests/frameworks/test_django.py index 15597f3d..f79642a6 100644 --- a/tests/frameworks/test_django.py +++ b/tests/frameworks/test_django.py @@ -11,7 +11,11 @@ from tests.apps.app_django import INSTALLED_APPS from instana.singletons import agent, tracer -from tests.helpers import fail_with_message_and_span_dump, get_first_span_by_filter, drop_log_spans_from_list +from tests.helpers import ( + fail_with_message_and_span_dump, + get_first_span_by_filter, + drop_log_spans_from_list, +) from instana.instrumentation.django.middleware import url_pattern_route apps.populate(INSTALLED_APPS) @@ -20,7 +24,7 @@ class TestDjango(StaticLiveServerTestCase): @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Setup and Teardown """ + """Setup and Teardown""" self.http = urllib3.PoolManager() self.recorder = tracer.span_processor # clear all spans before a test run @@ -31,7 +35,9 @@ def _resource(self) -> Generator[None, None, None]: def test_basic_request(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/', fields={"test": 1}) + response = self.http.request( + "GET", self.live_server_url + "/", fields={"test": 1} + ) assert response assert 200 == response.status @@ -43,20 +49,20 @@ def test_basic_request(self) -> None: urllib3_span = spans[1] django_span = spans[0] - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n @@ -73,21 +79,21 @@ def test_basic_request(self) -> None: assert test_span.sy is None assert django_span.ec is None - assert '/' == django_span.data["http"]["url"] - assert 'GET' == django_span.data["http"]["method"] + assert "/" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] assert 200 == django_span.data["http"]["status"] - assert 'test=1' == django_span.data["http"]["params"] - assert '^$' == django_span.data["http"]["path_tpl"] + assert "test=1" == django_span.data["http"]["params"] + assert "^$" == django_span.data["http"]["path_tpl"] assert django_span.stack is None def test_synthetic_request(self) -> None: - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } + headers = {"X-INSTANA-SYNTHETIC": "1"} with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/', headers=headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=headers + ) assert response assert 200 == response.status @@ -99,7 +105,7 @@ def test_synthetic_request(self) -> None: urllib3_span = spans[1] django_span = spans[0] - assert '^$' == django_span.data["http"]["path_tpl"] + assert "^$" == django_span.data["http"]["path_tpl"] assert django_span.sy assert urllib3_span.sy is None @@ -107,7 +113,7 @@ def test_synthetic_request(self) -> None: def test_request_with_error(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/cause_error') + response = self.http.request("GET", self.live_server_url + "/cause_error") assert response assert 500 == response.status @@ -120,32 +126,32 @@ def test_request_with_error(self) -> None: msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == 'sdk' and span.data['sdk']['name'] == 'test' + filter = lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" test_span = get_first_span_by_filter(spans, filter) assert test_span - filter = lambda span: span.n == 'urllib3' + filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, filter) assert urllib3_span - filter = lambda span: span.n == 'django' + filter = lambda span: span.n == "django" django_span = get_first_span_by_filter(spans, filter) assert django_span - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n @@ -159,16 +165,16 @@ def test_request_with_error(self) -> None: assert 1 == django_span.ec - assert '/cause_error' == django_span.data["http"]["url"] - assert 'GET' == django_span.data["http"]["method"] + assert "/cause_error" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] assert 500 == django_span.data["http"]["status"] - assert 'This is a fake error: /cause-error' == django_span.data["http"]["error"] - assert '^cause_error$' == django_span.data["http"]["path_tpl"] + assert "This is a fake error: /cause-error" == django_span.data["http"]["error"] + assert "^cause_error$" == django_span.data["http"]["path_tpl"] assert django_span.stack is None def test_request_with_not_found(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/not_found') + response = self.http.request("GET", self.live_server_url + "/not_found") assert response assert 404 == response.status @@ -181,7 +187,7 @@ def test_request_with_not_found(self) -> None: msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == 'django' + filter = lambda span: span.n == "django" django_span = get_first_span_by_filter(spans, filter) assert django_span @@ -190,7 +196,7 @@ def test_request_with_not_found(self) -> None: def test_request_with_not_found_no_route(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/no_route') + response = self.http.request("GET", self.live_server_url + "/no_route") assert response assert 404 == response.status @@ -203,7 +209,7 @@ def test_request_with_not_found_no_route(self) -> None: msg = "Expected 3 spans but got %d" % span_count fail_with_message_and_span_dump(msg, spans) - filter = lambda span: span.n == 'django' + filter = lambda span: span.n == "django" django_span = get_first_span_by_filter(spans, filter) assert django_span assert django_span.data["http"]["path_tpl"] is None @@ -212,7 +218,7 @@ def test_request_with_not_found_no_route(self) -> None: def test_complex_request(self) -> None: with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/complex') + response = self.http.request("GET", self.live_server_url + "/complex") assert response assert 200 == response.status @@ -225,20 +231,20 @@ def test_complex_request(self) -> None: otel_span1 = spans[1] otel_span2 = spans[0] - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value assert "test" == test_span.data["sdk"]["name"] assert "urllib3" == urllib3_span.n @@ -264,23 +270,22 @@ def test_complex_request(self) -> None: otel_span1.data["sdk"]["name"] == "asteroid" otel_span2.data["sdk"]["name"] == "spacedust" - assert '/complex' == django_span.data["http"]["url"] - assert 'GET' == django_span.data["http"]["method"] + assert "/complex" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] assert 200 == django_span.data["http"]["status"] - assert '^complex$' == django_span.data["http"]["path_tpl"] + assert "^complex$" == django_span.data["http"]["path_tpl"] def test_request_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) # response = self.client.get('/') assert response @@ -306,10 +311,10 @@ def test_request_header_capture(self) -> None: assert django_span.ec is None assert django_span.stack is None - assert '/' == django_span.data["http"]["url"] - assert 'GET' == django_span.data["http"]["method"] + assert "/" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] assert 200 == django_span.data["http"]["status"] - assert '^$' == django_span.data["http"]["path_tpl"] + assert "^$" == django_span.data["http"]["path_tpl"] assert "X-Capture-This" in django_span.data["http"]["header"] assert "this" == django_span.data["http"]["header"]["X-Capture-This"] @@ -321,10 +326,12 @@ def test_request_header_capture(self) -> None: def test_response_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = [u'X-Capture-This-Too', u'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] with tracer.start_as_current_span("test"): - response = self.http.request('GET', self.live_server_url + '/response_with_headers') + response = self.http.request( + "GET", self.live_server_url + "/response_with_headers" + ) assert response assert 200 == response.status @@ -349,10 +356,10 @@ def test_response_header_capture(self) -> None: assert django_span.ec is None assert django_span.stack is None - assert '/response_with_headers' == django_span.data["http"]["url"] - assert 'GET' == django_span.data["http"]["method"] + assert "/response_with_headers" == django_span.data["http"]["url"] + assert "GET" == django_span.data["http"]["method"] assert 200 == django_span.data["http"]["status"] - assert '^response_with_headers$' == django_span.data["http"]["path_tpl"] + assert "^response_with_headers$" == django_span.data["http"]["path_tpl"] assert "X-Capture-This-Too" in django_span.data["http"]["header"] assert "this too" == django_span.data["http"]["header"]["X-Capture-This-Too"] @@ -364,12 +371,18 @@ def test_response_header_capture(self) -> None: @pytest.mark.skip("Handled when type of trace and span ids are modified to str") def test_with_incoming_context(self) -> None: request_headers = dict() - request_headers['X-INSTANA-T'] = '1' - request_headers['X-INSTANA-S'] = '1' - request_headers['traceparent'] = '01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-788777' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' - - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + request_headers["X-INSTANA-T"] = "1" + request_headers["X-INSTANA-S"] = "1" + request_headers["traceparent"] = ( + "01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01-788777" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) + + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) assert response assert 200 == response.status @@ -384,39 +397,55 @@ def test_with_incoming_context(self) -> None: assert django_span.t == 1 assert django_span.p == 1 - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value - assert 'traceparent' in response.headers + assert "traceparent" in response.headers # The incoming traceparent header had version 01 (which does not exist at the time of writing), but since we # support version 00, we also need to pass down 00 for the version field. - assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - - assert 'tracestate' in response.headers - assert 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format(django_span.t, django_span.s) == response.headers['tracestate'] + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.t, django_span.s + ) + == response.headers["tracestate"] + ) @pytest.mark.skip("Handled when type of trace and span ids are modified to str") def test_with_incoming_context_and_correlation(self) -> None: request_headers = dict() - request_headers['X-INSTANA-T'] = '1' - request_headers['X-INSTANA-S'] = '1' - request_headers['X-INSTANA-L'] = '1, correlationType=web; correlationId=1234567890abcdef' - request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' - - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + request_headers["X-INSTANA-T"] = "1" + request_headers["X-INSTANA-S"] = "1" + request_headers["X-INSTANA-L"] = ( + "1, correlationType=web; correlationId=1234567890abcdef" + ) + request_headers["traceparent"] = ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) + + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) assert response assert 200 == response.status @@ -426,44 +455,57 @@ def test_with_incoming_context_and_correlation(self) -> None: django_span = spans[0] - assert django_span.t == 'a3ce929d0e0e4736' - assert django_span.p == '00f067aa0ba902b7' - assert django_span.ia.t == 'a3ce929d0e0e4736' - assert django_span.ia.p == '8357ccd9da194656' - assert django_span.lt == '4bf92f3577b34da6a3ce929d0e0e4736' - assert django_span.tp == True - assert django_span.crtp == 'web' - assert django_span.crid == '1234567890abcdef' - - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert django_span.t == "a3ce929d0e0e4736" + assert django_span.p == "00f067aa0ba902b7" + assert django_span.ia.t == "a3ce929d0e0e4736" + assert django_span.ia.p == "8357ccd9da194656" + assert django_span.lt == "4bf92f3577b34da6a3ce929d0e0e4736" + assert django_span.tp + assert django_span.crtp == "web" + assert django_span.crid == "1234567890abcdef" + + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value - - assert 'traceparent' in response.headers - assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - - assert 'tracestate' in response.headers - assert 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s) == response.headers['tracestate'] + assert response.headers["Server-Timing"] == server_timing_value + + assert "traceparent" in response.headers + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.t, django_span.s + ) + == response.headers["tracestate"] + ) @pytest.mark.skip("Handled when type of trace and span ids are modified to str") def test_with_incoming_traceparent_tracestate(self) -> None: request_headers = dict() - request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' + request_headers["traceparent"] = ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) assert response assert 200 == response.status @@ -473,43 +515,58 @@ def test_with_incoming_traceparent_tracestate(self) -> None: django_span = spans[0] - assert django_span.t == 'a3ce929d0e0e4736' # last 16 chars from traceparent trace_id - assert django_span.p == '00f067aa0ba902b7' - assert django_span.ia.t == 'a3ce929d0e0e4736' - assert django_span.ia.p == '8357ccd9da194656' - assert django_span.lt == '4bf92f3577b34da6a3ce929d0e0e4736' - assert django_span.tp == True - - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert ( + django_span.t == "a3ce929d0e0e4736" + ) # last 16 chars from traceparent trace_id + assert django_span.p == "00f067aa0ba902b7" + assert django_span.ia.t == "a3ce929d0e0e4736" + assert django_span.ia.p == "8357ccd9da194656" + assert django_span.lt == "4bf92f3577b34da6a3ce929d0e0e4736" + assert django_span.tp + + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value - - assert 'traceparent' in response.headers - assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - - assert 'tracestate' in response.headers - assert 'in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.s) == response.headers['tracestate'] + assert response.headers["Server-Timing"] == server_timing_value + + assert "traceparent" in response.headers + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in=a3ce929d0e0e4736;{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.s + ) + == response.headers["tracestate"] + ) @pytest.mark.skip("Handled when type of trace and span ids are modified to str") def test_with_incoming_traceparent_tracestate_disable_traceparent(self) -> None: os.environ["INSTANA_DISABLE_W3C_TRACE_CORRELATION"] = "1" request_headers = dict() - request_headers['traceparent'] = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' - request_headers['tracestate'] = 'rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE' + request_headers["traceparent"] = ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ) + request_headers["tracestate"] = ( + "rojo=00f067aa0ba902b7,in=a3ce929d0e0e4736;8357ccd9da194656,congo=t61rcWkgMzE" + ) - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) assert response assert 200 == response.status @@ -519,37 +576,48 @@ def test_with_incoming_traceparent_tracestate_disable_traceparent(self) -> None: django_span = spans[0] - assert django_span.t == 'a3ce929d0e0e4736' # last 16 chars from traceparent trace_id - assert django_span.p == '8357ccd9da194656' + assert ( + django_span.t == "a3ce929d0e0e4736" + ) # last 16 chars from traceparent trace_id + assert django_span.p == "8357ccd9da194656" - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value - - assert 'traceparent' in response.headers - assert '00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01'.format(django_span.s) == response.headers['traceparent'] - - assert 'tracestate' in response.headers - assert 'in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE'.format( - django_span.t, django_span.s) == response.headers['tracestate'] + assert response.headers["Server-Timing"] == server_timing_value + + assert "traceparent" in response.headers + assert ( + "00-4bf92f3577b34da6a3ce929d0e0e4736-{}-01".format(django_span.s) + == response.headers["traceparent"] + ) + + assert "tracestate" in response.headers + assert ( + "in={};{},rojo=00f067aa0ba902b7,congo=t61rcWkgMzE".format( + django_span.t, django_span.s + ) + == response.headers["tracestate"] + ) def test_with_incoming_mixed_case_context(self) -> None: request_headers = dict() - request_headers['X-InSTANa-T'] = '0000000000000001' - request_headers['X-instana-S'] = '0000000000000001' + request_headers["X-InSTANa-T"] = "0000000000000001" + request_headers["X-instana-S"] = "0000000000000001" - response = self.http.request('GET', self.live_server_url + '/', headers=request_headers) + response = self.http.request( + "GET", self.live_server_url + "/", headers=request_headers + ) assert response assert 200 == response.status @@ -564,27 +632,27 @@ def test_with_incoming_mixed_case_context(self) -> None: assert django_span.t == 1 assert django_span.p == 1 - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) assert response.headers["X-INSTANA-T"] == str(django_span.t) - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) assert response.headers["X-INSTANA-S"] == str(django_span.s) - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - assert 'Server-Timing' in response.headers + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % django_span.t - assert response.headers['Server-Timing'] == server_timing_value + assert response.headers["Server-Timing"] == server_timing_value def test_url_pattern_route(self) -> None: - view_name="app_django.another" + view_name = "app_django.another" path_tpl = "".join(url_pattern_route(view_name)) assert path_tpl == "^another$" - - view_name="app_django.complex" + + view_name = "app_django.complex" try: path_tpl = "".join(url_pattern_route(view_name)) except Exception: From 6ad1f943a64e7edf9cb62a20e90b4d638463a149 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 21 Aug 2024 17:58:41 +0200 Subject: [PATCH 120/172] fix: Update SpanAttributes in ASGI instrumentation Signed-off-by: Paulo Vital --- src/instana/instrumentation/asgi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/instana/instrumentation/asgi.py b/src/instana/instrumentation/asgi.py index 8e2fb825..ed0866ae 100644 --- a/src/instana/instrumentation/asgi.py +++ b/src/instana/instrumentation/asgi.py @@ -39,7 +39,7 @@ def _extract_custom_headers( for header_pair in headers: if header_pair[0].decode("utf-8").lower() == custom_header.lower(): span.set_attribute( - "http.header.%s" % custom_header, + f"http.header.{custom_header}", header_pair[1].decode("utf-8"), ) except Exception: @@ -49,11 +49,11 @@ def _collect_kvs(self, scope: Dict[str, Any], span: "InstanaSpan") -> None: try: span.set_attribute("span.kind", SpanKind.SERVER) span.set_attribute("http.path", scope.get("path")) - span.set_attribute("http.method", scope.get("method")) + span.set_attribute(SpanAttributes.HTTP_METHOD, scope.get("method")) server = scope.get("server") if isinstance(server, tuple) or isinstance(server, list): - span.set_attribute("http.host", server[0]) + span.set_attribute(SpanAttributes.HTTP_HOST, server[0]) query = scope.get("query_string") if isinstance(query, (str, bytes)) and len(query): From 15dcdef7e26105bb1b4f86d273f8eebcf7c8022d Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 20 Aug 2024 13:31:51 +0200 Subject: [PATCH 121/172] refactor: FastAPI instrumentation. Signed-off-by: Paulo Vital --- src/instana/__init__.py | 2 +- src/instana/instrumentation/fastapi_inst.py | 73 +++++++++++++-------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index ab82fd54..2ff11202 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -167,7 +167,7 @@ def boot_agent(): # boto3_inst, # noqa: F401 # cassandra_inst, # noqa: F401 # couchbase_inst, # noqa: F401 - # fastapi_inst, # noqa: F401 + fastapi_inst, # noqa: F401 flask, # noqa: F401 # gevent_inst, # noqa: F401 # grpcio, # noqa: F401 diff --git a/src/instana/instrumentation/fastapi_inst.py b/src/instana/instrumentation/fastapi_inst.py index c2d56d84..5edee85c 100644 --- a/src/instana/instrumentation/fastapi_inst.py +++ b/src/instana/instrumentation/fastapi_inst.py @@ -5,65 +5,86 @@ Instrumentation for FastAPI https://fastapi.tiangolo.com/ """ + +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple + try: - import fastapi import os - import wrapt import signal - from ..log import logger - from ..util.gunicorn import running_in_gunicorn - from .asgi import InstanaASGIMiddleware - from starlette.middleware import Middleware + import fastapi + import wrapt from fastapi import HTTPException from fastapi.exception_handlers import http_exception_handler + from starlette.middleware import Middleware + + from instana.instrumentation.asgi import InstanaASGIMiddleware + from instana.log import logger + from instana.util.gunicorn import running_in_gunicorn + from instana.util.traceutils import get_tracer_tuple + + from opentelemetry.semconv.trace import SpanAttributes - from instana.singletons import async_tracer + if TYPE_CHECKING: + from starlette.requests import Request + from starlette.responses import Response - if not(hasattr(fastapi, '__version__') - and (fastapi.__version__[0] > '0' or - int(fastapi.__version__.split('.')[1]) >= 51)): - logger.debug('Instana supports FastAPI package versions 0.51.0 and newer. Skipping.') + if not ( # pragma: no cover + hasattr(fastapi, "__version__") + and ( + fastapi.__version__[0] > "0" or int(fastapi.__version__.split(".")[1]) >= 51 + ) + ): + logger.debug( + "Instana supports FastAPI package versions 0.51.0 and newer. Skipping." + ) raise ImportError - async def instana_exception_handler(request, exc): + async def instana_exception_handler( + request: "Request", exc: HTTPException + ) -> "Response": """ We capture FastAPI HTTPException, log the error and pass it on to the default exception handler. """ try: - span = async_tracer.active_span + _, span, _ = get_tracer_tuple() - if span is not None: - if hasattr(exc, 'detail') and 500 <= exc.status_code: - span.set_tag('http.error', exc.detail) - span.set_tag('http.status_code', exc.status_code) + if span: + if hasattr(exc, "detail") and 500 <= exc.status_code: + span.set_attribute("http.error", exc.detail) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, exc.status_code) except Exception: logger.debug("FastAPI instana_exception_handler: ", exc_info=True) return await http_exception_handler(request, exc) - @wrapt.patch_function_wrapper('fastapi.applications', 'FastAPI.__init__') - def init_with_instana(wrapped, instance, args, kwargs): - middleware = kwargs.get('middleware') + @wrapt.patch_function_wrapper("fastapi.applications", "FastAPI.__init__") + def init_with_instana( + wrapped: Callable[..., fastapi.applications.FastAPI.__init__], + instance: fastapi.applications.FastAPI, + args: Tuple, + kwargs: Dict[str, Any], + ) -> None: + middleware = kwargs.get("middleware") if middleware is None: - kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)] + kwargs["middleware"] = [Middleware(InstanaASGIMiddleware)] elif isinstance(middleware, list): middleware.append(Middleware(InstanaASGIMiddleware)) - exception_handlers = kwargs.get('exception_handlers') + exception_handlers = kwargs.get("exception_handlers") if exception_handlers is None: - kwargs['exception_handlers'] = dict() + kwargs["exception_handlers"] = dict() - if isinstance(kwargs['exception_handlers'], dict): - kwargs['exception_handlers'][HTTPException] = instana_exception_handler + if isinstance(kwargs["exception_handlers"], dict): + kwargs["exception_handlers"][HTTPException] = instana_exception_handler return wrapped(*args, **kwargs) logger.debug("Instrumenting FastAPI") # Reload GUnicorn when we are instrumenting an already running application - if "INSTANA_MAGIC" in os.environ and running_in_gunicorn(): + if "INSTANA_MAGIC" in os.environ and running_in_gunicorn(): # pragma: no cover os.kill(os.getpid(), signal.SIGHUP) except ImportError: From b4f02207cabe85307f1616d933936ec8756beeae Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 20 Aug 2024 13:32:16 +0200 Subject: [PATCH 122/172] tests(fastapi): adapt tests to OTel usage. Signed-off-by: Paulo Vital --- tests/apps/fastapi_app/app.py | 28 +- tests/apps/fastapi_app/app2.py | 21 + tests/conftest.py | 1 - tests/frameworks/test_fastapi.py | 1087 +++++++++---------- tests/frameworks/test_fastapi_middleware.py | 96 ++ 5 files changed, 656 insertions(+), 577 deletions(-) create mode 100644 tests/apps/fastapi_app/app2.py create mode 100644 tests/frameworks/test_fastapi_middleware.py diff --git a/tests/apps/fastapi_app/app.py b/tests/apps/fastapi_app/app.py index 1666ecd8..1040c141 100644 --- a/tests/apps/fastapi_app/app.py +++ b/tests/apps/fastapi_app/app.py @@ -1,14 +1,10 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -from ...helpers import testenv - from fastapi import FastAPI, HTTPException, Response -from fastapi.exceptions import RequestValidationError -from fastapi.responses import PlainTextResponse from fastapi.concurrency import run_in_threadpool +from fastapi.testclient import TestClient from starlette.exceptions import HTTPException as StarletteHTTPException -import requests fastapi_server = FastAPI() @@ -20,48 +16,58 @@ # async def validation_exception_handler(request, exc): # return PlainTextResponse(str(exc), status_code=400) + @fastapi_server.get("/") async def root(): return {"message": "Hello World"} + @fastapi_server.get("/users/{user_id}") async def user(user_id): return {"user": user_id} + @fastapi_server.get("/response_headers") async def response_headers(): - headers = { - 'X-Capture-This-Too': 'this too', - 'X-Capture-That-Too': 'that too' - } + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} return Response("Stan wuz here with headers!", headers=headers) + @fastapi_server.get("/400") async def four_zero_zero(): raise HTTPException(status_code=400, detail="400 response") + @fastapi_server.get("/404") async def four_zero_four(): raise HTTPException(status_code=404, detail="Item not found") + @fastapi_server.get("/500") async def five_hundred(): raise HTTPException(status_code=500, detail="500 response") + @fastapi_server.get("/starlette_exception") async def starlette_exception(): raise StarletteHTTPException(status_code=500, detail="500 response") + def trigger_outgoing_call(): - response = requests.get(testenv["fastapi_server"]+"/users/1") + client = TestClient(fastapi_server) + response = client.get("/users/1") return response.json() + @fastapi_server.get("/non_async_simple") def non_async_complex_call(): response = trigger_outgoing_call() return response + @fastapi_server.get("/non_async_threadpool") def non_async_threadpool(): run_in_threadpool(trigger_outgoing_call) - return {"message": "non async functions executed on a thread pool can't be followed through thread boundaries"} \ No newline at end of file + return { + "message": "non async functions executed on a thread pool can't be followed through thread boundaries" + } diff --git a/tests/apps/fastapi_app/app2.py b/tests/apps/fastapi_app/app2.py new file mode 100644 index 00000000..8f9b7edd --- /dev/null +++ b/tests/apps/fastapi_app/app2.py @@ -0,0 +1,21 @@ +# (c) Copyright IBM Corp. 2024 + +from fastapi import FastAPI, HTTPException, Response +from fastapi.concurrency import run_in_threadpool +from fastapi.middleware import Middleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware + + +fastapi_server = FastAPI( + middleware=[ + Middleware( + TrustedHostMiddleware, + allowed_hosts=["*"], + ), + ], +) + + +@fastapi_server.get("/") +async def root(): + return {"message": "Hello World"} diff --git a/tests/conftest.py b/tests/conftest.py index c6078e5e..9e6554fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,6 @@ collect_ignore_glob.append("*frameworks/test_aiohttp*") collect_ignore_glob.append("*frameworks/test_asyncio*") collect_ignore_glob.append("*frameworks/test_celery*") -collect_ignore_glob.append("*frameworks/test_fastapi*") collect_ignore_glob.append("*frameworks/test_gevent*") collect_ignore_glob.append("*frameworks/test_grpcio*") collect_ignore_glob.append("*frameworks/test_pyramid*") diff --git a/tests/frameworks/test_fastapi.py b/tests/frameworks/test_fastapi.py index 6a276e26..7e82df22 100644 --- a/tests/frameworks/test_fastapi.py +++ b/tests/frameworks/test_fastapi.py @@ -1,628 +1,585 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import time -import unittest -import multiprocessing - -import requests - -from instana.singletons import async_tracer -from tests.apps.fastapi_app import launch_fastapi -from ..helpers import testenv -from ..helpers import get_first_span_by_filter - - -class TestFastAPI(unittest.TestCase): - def setUp(self): - self.proc = multiprocessing.Process(target=launch_fastapi, args=(), daemon=True) - self.proc.start() - time.sleep(2) - - def tearDown(self): - # Kill server after tests - self.proc.kill() - - def test_vanilla_get(self): - result = requests.get(testenv["fastapi_server"] + "/") - - self.assertEqual(result.status_code, 200) - self.assertIn("X-INSTANA-T", result.headers) - self.assertIn("X-INSTANA-S", result.headers) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - self.assertIn("Server-Timing", result.headers) - - spans = async_tracer.recorder.queued_spans() - # FastAPI instrumentation (like all instrumentation) _always_ traces unless told otherwise - self.assertEqual(len(spans), 1) - self.assertEqual(spans[0].n, "asgi") - - def test_basic_get(self): +from typing import Generator + +from fastapi.testclient import TestClient +import pytest +from instana.singletons import tracer, agent + +from tests.apps.fastapi_app.app import fastapi_server +from tests.helpers import get_first_span_by_filter + + +class TestFastAPI: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from Starlette/FastAPI to make it easier. + self.client = TestClient(fastapi_server) + + # Clear all spans before a test run + self.recorder = tracer.span_processor + self.recorder.clear_spans() + + # Hack together a manual custom headers list; We'll use this in tests + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + "X-Capture-This-Too", + "X-Capture-That-Too", + ] + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # FastAPI instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_400(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_400(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/400") - - self.assertEqual(result.status_code, 400) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/400", headers=headers) + + assert result + assert result.status_code == 400 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/400") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/400") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 400) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_500(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/400" + assert asgi_span.data["http"]["path_tpl"] == "/400" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 400 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_500(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/500") - - self.assertEqual(result.status_code, 500) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/500", headers=headers) + + assert result + assert result.status_code == 500 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertEqual(asgi_span.ec, 1) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/500") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/500") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 500) - self.assertEqual(asgi_span.data["http"]["error"], "500 response") - - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_path_templates(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/500" + assert asgi_span.data["http"]["path_tpl"] == "/500" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert asgi_span.data["http"]["error"] == "500 response" + assert not asgi_span.data["http"]["params"] + + def test_path_templates(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/users/1") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/users/1", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/users/1") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/users/{user_id}") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - def test_secret_scrubbing(self): + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/users/1" + assert asgi_span.data["http"]["path_tpl"] == "/users/{user_id}" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_secret_scrubbing(self) -> None: result = None - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/?secret=shhh") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/?secret=shhh", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertEqual(asgi_span.data["http"]["params"], "secret=") - - def test_synthetic_request(self): - request_headers = {"X-INSTANA-SYNTHETIC": "1"} - with async_tracer.start_active_span("test"): - result = requests.get( - testenv["fastapi_server"] + "/", headers=request_headers - ) - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert asgi_span.data["http"]["params"] == "secret=" + + def test_synthetic_request(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-INSTANA-SYNTHETIC": "1", + } + result = self.client.get("/", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertTrue(asgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) - - def test_request_header_capture(self): - from instana.singletons import agent - - # The background FastAPI server is pre-configured with custom headers to capture - - request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} - - with async_tracer.start_active_span("test"): - result = requests.get( - testenv["fastapi_server"] + "/", headers=request_headers - ) - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert asgi_span.sy + assert not test_span.sy + + def test_request_header_capture(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-Capture-This": "this", + "X-Capture-That": "that", + } + result = self.client.get("/", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertIn("X-Capture-This", asgi_span.data["http"]["header"]) - self.assertEqual("this", asgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", asgi_span.data["http"]["header"]) - self.assertEqual("that", asgi_span.data["http"]["header"]["X-Capture-That"]) - - def test_response_header_capture(self): - from instana.singletons import agent - - # The background FastAPI server is pre-configured with custom headers to capture - - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/response_headers") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-That"] == "that" + + def test_response_header_capture(self) -> None: + # The background FastAPI server is pre-configured with custom headers + # to capture. + + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/response_headers", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) + assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) - - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertIn("X-Capture-This-Too", asgi_span.data["http"]["header"]) - self.assertEqual("this too", asgi_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", asgi_span.data["http"]["header"]) - self.assertEqual("that too", asgi_span.data["http"]["header"]["X-Capture-That-Too"]) - - def test_non_async_simple(self): - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/non_async_simple") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(5, len(spans)) - - span_filter = ( + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/response_headers" + assert asgi_span.data["http"]["path_tpl"] == "/response_headers" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This-Too" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in asgi_span.data["http"]["header"] + assert asgi_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" + + def test_non_async_simple(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/non_async_simple", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = ( - lambda span: span.n == "urllib3" and span.p == test_span.s - ) - urllib3_span1 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span1) + assert test_span - span_filter = ( - lambda span: span.n == "asgi" and span.p == urllib3_span1.s - ) + span_filter = lambda span: span.n == "asgi" and span.p == test_span.s # noqa: E731 asgi_span1 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span1) - - span_filter = ( - lambda span: span.n == "urllib3" and span.p == asgi_span1.s - ) - urllib3_span2 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span2) + assert asgi_span1 - span_filter = ( - lambda span: span.n == "asgi" and span.p == urllib3_span2.s - ) + span_filter = lambda span: span.n == "asgi" and span.p == asgi_span1.s # noqa: E731 asgi_span2 = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span2) + assert asgi_span2 # Same traceId traceId = test_span.t - self.assertEqual(traceId, urllib3_span1.t) - self.assertEqual(traceId, asgi_span1.t) - self.assertEqual(traceId, urllib3_span2.t) - self.assertEqual(traceId, asgi_span2.t) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span1.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span1.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span1.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span1.ec) - self.assertEqual(asgi_span1.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span1.data["http"]["path"], "/non_async_simple") - self.assertEqual(asgi_span1.data["http"]["path_tpl"], "/non_async_simple") - self.assertEqual(asgi_span1.data["http"]["method"], "GET") - self.assertEqual(asgi_span1.data["http"]["status"], 200) - - self.assertIsNone(asgi_span1.data["http"]["error"]) - self.assertIsNone(asgi_span1.data["http"]["params"]) - - def test_non_async_threadpool(self): - with async_tracer.start_active_span("test"): - result = requests.get(testenv["fastapi_server"] + "/non_async_threadpool") - - self.assertEqual(result.status_code, 200) - - spans = async_tracer.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - span_filter = ( + assert asgi_span1.t == traceId + assert asgi_span2.t == traceId + + assert result.headers["X-INSTANA-T"] == str(asgi_span1.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span1.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span1.t}" + + assert not asgi_span1.ec + assert asgi_span1.data["http"]["host"] == "testserver" + assert asgi_span1.data["http"]["path"] == "/non_async_simple" + assert asgi_span1.data["http"]["path_tpl"] == "/non_async_simple" + assert asgi_span1.data["http"]["method"] == "GET" + assert asgi_span1.data["http"]["status"] == 200 + assert not asgi_span1.data["http"]["error"] + assert not asgi_span1.data["http"]["params"] + + assert not asgi_span2.ec + assert asgi_span2.data["http"]["host"], "testserver" + assert asgi_span2.data["http"]["path"], "/users/1" + assert asgi_span2.data["http"]["path_tpl"], "/users/{user_id}" + assert asgi_span2.data["http"]["method"], "GET" + assert asgi_span2.data["http"]["status"], 200 + assert not asgi_span2.data["http"]["error"] + assert not asgi_span2.data["http"]["params"] + + def test_non_async_threadpool(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/non_async_threadpool", headers=headers) + + assert result + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(test_span) - - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(urllib3_span) + assert test_span - span_filter = lambda span: span.n == "asgi" + span_filter = lambda span: span.n == "asgi" # noqa: E731 asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertTrue(asgi_span) - - # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, asgi_span.t) - - # Parent relationships - self.assertEqual(asgi_span.p, urllib3_span.s) - self.assertEqual(urllib3_span.p, test_span.s) - - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - - self.assertIn("Server-Timing", result.headers) - server_timing_value = "intid;desc=%s" % asgi_span.t - self.assertEqual(result.headers["Server-Timing"], server_timing_value) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data["http"]["host"], "127.0.0.1") - self.assertEqual(asgi_span.data["http"]["path"], "/non_async_threadpool") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/non_async_threadpool") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/non_async_threadpool" + assert asgi_span.data["http"]["path_tpl"] == "/non_async_threadpool" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] diff --git a/tests/frameworks/test_fastapi_middleware.py b/tests/frameworks/test_fastapi_middleware.py new file mode 100644 index 00000000..5c915f25 --- /dev/null +++ b/tests/frameworks/test_fastapi_middleware.py @@ -0,0 +1,96 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import logging +from typing import Generator + +import pytest +from instana.singletons import tracer +from fastapi.testclient import TestClient + +from tests.helpers import get_first_span_by_filter + + +class TestFastAPIMiddleware: + """ + Tests FastAPI with provided Middleware. + """ + + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # We are using the TestClient from FastAPI to make it easier. + from tests.apps.fastapi_app.app2 import fastapi_server + self.client = TestClient(fastapi_server) + # Clear all spans before a test run. + self.recorder = tracer.span_processor + self.recorder.clear_spans() + yield + del fastapi_server + + def test_vanilla_get(self) -> None: + result = self.client.get("/") + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + # FastAPI instrumentation (like all instrumentation) _always_ traces + # unless told otherwise + spans = self.recorder.queued_spans() + + assert len(spans) == 1 + assert spans[0].n == "asgi" + + def test_basic_get(self) -> None: + result = None + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + result = self.client.get("/", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + # TODO: after support httpx, the expected value will be 3. + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span + + span_filter = lambda span: span.n == "asgi" # noqa: E731 + asgi_span = get_first_span_by_filter(spans, span_filter) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert result.headers["Server-Timing"] == f"intid;desc={asgi_span.t}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert asgi_span.data["http"]["host"] == "testserver" + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] From ca3eda226cfa39ba2650675a116a81b2bbcb59ce Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 26 Aug 2024 09:42:29 +0530 Subject: [PATCH 123/172] fix: handle suppression Signed-off-by: Varsha GS --- src/instana/propagators/base_propagator.py | 8 +++----- src/instana/tracer.py | 6 +++--- tests/frameworks/test_flask.py | 4 +--- tests/frameworks/test_wsgi.py | 1 - 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index eb779d1d..3e4fcdc8 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -181,8 +181,6 @@ def __determine_span_context( correlation = True ( - ctx_trace_id, - ctx_span_id, ctx_level, ctx_synthetic, ctx_trace_parent, @@ -192,9 +190,11 @@ def __determine_span_context( ctx_correlation_id, ctx_traceparent, ctx_tracestate, - ) = [None] * 11 + ) = [None] * 9 ctx_level = self._get_ctx_level(level) + ctx_trace_id = trace_id + ctx_span_id = span_id if ( trace_id @@ -204,8 +204,6 @@ def __determine_span_context( ): # ctx.trace_id = trace_id[-16:] # only the last 16 chars # ctx.span_id = span_id[-16:] # only the last 16 chars - ctx_trace_id = trace_id - ctx_span_id = span_id ctx_synthetic = synthetic # if len(trace_id) > 16: diff --git a/src/instana/tracer.py b/src/instana/tracer.py index 5f08f729..a5bdf895 100644 --- a/src/instana/tracer.py +++ b/src/instana/tracer.py @@ -120,10 +120,10 @@ def start_span( ) -> InstanaSpan: parent_context = span_context if span_context else get_current_span().get_span_context() - if parent_context is not None and not isinstance(parent_context, SpanContext): + if parent_context and not isinstance(parent_context, SpanContext): raise TypeError("parent_context must be an Instana SpanContext or None.") - if parent_context is not None and not parent_context.is_valid: + if parent_context and not parent_context.is_valid and not parent_context.suppression: # We probably have an INVALID_SPAN_CONTEXT. parent_context = None @@ -213,7 +213,7 @@ def _add_stack(self, span: InstanaSpan, limit: Optional[int] = 30) -> None: def _create_span_context(self, parent_context: SpanContext) -> SpanContext: """Creates a new SpanContext based on the given parent context.""" - if parent_context is not None and parent_context.trace_id is not None: + if parent_context and parent_context.is_valid: trace_id = parent_context.trace_id span_id = generate_id() trace_flags = parent_context.trace_flags diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index da1fd3c2..145d0e33 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -191,7 +191,6 @@ def test_get_request_with_query_params(self) -> None: # We should NOT have a path template for this route assert wsgi_span.data["http"]["path_tpl"] is None - @unittest.skip("Suppression is not yet handled") def test_get_request_with_suppression(self) -> None: headers = {'X-INSTANA-L':'0'} response = self.http.urlopen('GET', testenv["flask_server"] + '/', headers=headers) @@ -213,7 +212,7 @@ def test_get_request_with_suppression(self) -> None: # Assert that there are no spans in the recorded list assert spans == [] - @unittest.skip("Suppression is not yet handled") + @unittest.skip("Handled when type of trace and span ids are modified to str") def test_get_request_with_suppression_and_w3c(self) -> None: headers = { 'X-INSTANA-L':'0', @@ -239,7 +238,6 @@ def test_get_request_with_suppression_and_w3c(self) -> None: # Assert that there are no spans in the recorded list assert spans == [] - @unittest.skip("Synthetic requests are not yet handled") def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index 186d84f7..7e9f3484 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -86,7 +86,6 @@ def test_get_request(self) -> None: assert wsgi_span.data["http"]["error"] is None assert wsgi_span.stack is None - @pytest.mark.skip("Suppression is not yet handled") def test_synthetic_request(self) -> None: headers = { 'X-INSTANA-SYNTHETIC': '1' From 2477bcc281e273302c56c21162a0330a173f1ba8 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 26 Aug 2024 15:48:12 +0530 Subject: [PATCH 124/172] fix: test_non_async_simple Signed-off-by: Varsha GS --- tests/apps/fastapi_app/app.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/apps/fastapi_app/app.py b/tests/apps/fastapi_app/app.py index 1040c141..eac3662e 100644 --- a/tests/apps/fastapi_app/app.py +++ b/tests/apps/fastapi_app/app.py @@ -5,6 +5,7 @@ from fastapi.concurrency import run_in_threadpool from fastapi.testclient import TestClient from starlette.exceptions import HTTPException as StarletteHTTPException +from instana.span.span import get_current_span fastapi_server = FastAPI() @@ -54,7 +55,14 @@ async def starlette_exception(): def trigger_outgoing_call(): - client = TestClient(fastapi_server) + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = get_current_span().get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + client = TestClient(fastapi_server, headers=headers) response = client.get("/users/1") return response.json() From 01601e1e06c289a28aef6dbb2e4fc8483de32ab4 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 28 Aug 2024 21:28:38 +0530 Subject: [PATCH 125/172] fix: starlette import error Signed-off-by: Varsha GS --- src/instana/instrumentation/starlette_inst.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/instana/instrumentation/starlette_inst.py b/src/instana/instrumentation/starlette_inst.py index 4edf4b4e..9d4b1f8e 100644 --- a/src/instana/instrumentation/starlette_inst.py +++ b/src/instana/instrumentation/starlette_inst.py @@ -8,12 +8,11 @@ from typing import Any, Callable, Dict, Tuple -import starlette.applications - try: import starlette import wrapt from starlette.middleware import Middleware + import starlette.applications from instana.instrumentation.asgi import InstanaASGIMiddleware from instana.log import logger @@ -34,5 +33,6 @@ def init_with_instana( return wrapped(*args, **kwargs) logger.debug("Instrumenting Starlette") + except ImportError: pass From 0898d3bf2f246c50fb31320de716743de3874324 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 28 Aug 2024 21:38:56 +0530 Subject: [PATCH 126/172] fix(circleci): exclude google cloud jobs till the instrumentation is done Signed-off-by: Varsha GS --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8bbe4f55..758807c3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -367,8 +367,8 @@ workflows: # - py39cassandra # - py39couchbase # - py39gevent_starlette - - py311googlecloud - - py312googlecloud + # - py311googlecloud + # - py312googlecloud - final_job: requires: - python38 @@ -380,5 +380,5 @@ workflows: # - py39cassandra # - py39couchbase # - py39gevent_starlette - - py311googlecloud - - py312googlecloud + # - py311googlecloud + # - py312googlecloud From 9f8fcd58226de22dae199d6c142a6b7ac116ad3c Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 28 Aug 2024 21:20:56 +0530 Subject: [PATCH 127/172] fix: Flaky test in urllib3 Signed-off-by: Varsha GS --- tests/clients/test_urllib3.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index 5c735614..77b49ece 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -137,8 +137,9 @@ def test_get_request(self): assert len(urllib3_span.stack) > 1 def test_get_request_https(self): + request_url = "https://reqres.in:443/api/users" with tracer.start_as_current_span("test"): - r = self.http.request("GET", "https://httpbin.org/robots.txt") + r = self.http.request("GET", request_url) spans = self.recorder.queued_spans() assert len(spans) == 2 @@ -163,7 +164,7 @@ def test_get_request_https(self): assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 - assert urllib3_span.data["http"]["url"] == "https://httpbin.org:443/robots.txt" + assert urllib3_span.data["http"]["url"] == request_url assert urllib3_span.data["http"]["method"] == "GET" assert urllib3_span.stack assert isinstance(urllib3_span.stack, list) From 230baefa73763061c6913679939391398bcdb135 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 21 Aug 2024 21:59:59 +0530 Subject: [PATCH 128/172] boto3: refactor instrumentation Signed-off-by: Varsha GS --- src/instana/instrumentation/boto3_inst.py | 62 +++++++++++++---------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/src/instana/instrumentation/boto3_inst.py b/src/instana/instrumentation/boto3_inst.py index e4099595..f5aa2b27 100644 --- a/src/instana/instrumentation/boto3_inst.py +++ b/src/instana/instrumentation/boto3_inst.py @@ -6,12 +6,13 @@ import wrapt import inspect -from ..log import logger -from ..singletons import tracer, agent -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.log import logger +from instana.singletons import tracer, agent +from instana.util.traceutils import get_tracer_tuple, tracing_is_off +from instana.propagators.format import Format +from instana.span.span import get_current_span try: - import opentracing as ot import boto3 from boto3.s3 import inject @@ -21,13 +22,13 @@ def extract_custom_headers(span, headers): try: for custom_header in agent.options.extra_http_headers: if custom_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[custom_header]) + span.set_attribute("http.header.%s" % custom_header, headers[custom_header]) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - def lambda_inject_context(payload, scope): + def lambda_inject_context(payload, span): """ When boto3 lambda client 'Invoke' is called, we want to inject the tracing context. boto3/botocore has specific requirements: @@ -39,7 +40,7 @@ def lambda_inject_context(payload, scope): if not isinstance(invoke_payload, dict): invoke_payload = json.loads(invoke_payload) - tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, invoke_payload) + tracer.inject(span.context, Format.HTTP_HEADERS, invoke_payload) payload['Payload'] = json.dumps(invoke_payload) except Exception: logger.debug("non-fatal lambda_inject_context: ", exc_info=True) @@ -47,8 +48,9 @@ def lambda_inject_context(payload, scope): @wrapt.patch_function_wrapper("botocore.auth", "SigV4Auth.add_auth") def emit_add_auth_with_instana(wrapped, instance, args, kwargs): - if not tracing_is_off() and tracer.active_span: - extract_custom_headers(tracer.active_span, args[0].headers) + current_span = get_current_span() + if not tracing_is_off() and current_span and current_span.is_recording(): + extract_custom_headers(current_span, args[0].headers) return wrapped(*args, **kwargs) @@ -60,25 +62,27 @@ def make_api_call_with_instana(wrapped, instance, arg_list, kwargs): tracer, parent_span, _ = get_tracer_tuple() - with tracer.start_active_span("boto3", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span("boto3", span_context=parent_context) as span: try: operation = arg_list[0] payload = arg_list[1] - scope.span.set_tag('op', operation) - scope.span.set_tag('ep', instance._endpoint.host) - scope.span.set_tag('reg', instance._client_config.region_name) + span.set_attribute('op', operation) + span.set_attribute('ep', instance._endpoint.host) + span.set_attribute('reg', instance._client_config.region_name) - scope.span.set_tag('http.url', instance._endpoint.host + ':443/' + arg_list[0]) - scope.span.set_tag('http.method', 'POST') + span.set_attribute('http.url', instance._endpoint.host + ':443/' + arg_list[0]) + span.set_attribute('http.method', 'POST') # Don't collect payload for SecretsManager if not hasattr(instance, 'get_secret_value'): - scope.span.set_tag('payload', payload) + span.set_attribute('payload', payload) # Inject context when invoking lambdas if 'lambda' in instance._endpoint.host and operation == 'Invoke': - lambda_inject_context(payload, scope) + lambda_inject_context(payload, span) except Exception as exc: logger.debug("make_api_call_with_instana: collect error", exc_info=True) @@ -91,13 +95,13 @@ def make_api_call_with_instana(wrapped, instance, arg_list, kwargs): if isinstance(http_dict, dict): status = http_dict.get('HTTPStatusCode') if status is not None: - scope.span.set_tag('http.status_code', status) + span.set_attribute('http.status_code', status) headers = http_dict.get('HTTPHeaders') - extract_custom_headers(scope.span, headers) + extract_custom_headers(span, headers) return result except Exception as exc: - scope.span.mark_as_errored({'error': exc}) + span.mark_as_errored({'error': exc}) raise @@ -112,15 +116,17 @@ def s3_inject_method_with_instana(wrapped, instance, arg_list, kwargs): tracer, parent_span, _ = get_tracer_tuple() - with tracer.start_active_span("boto3", child_of=parent_span) as scope: + parent_context = parent_span.get_span_context() if parent_span else None + + with tracer.start_as_current_span("boto3", span_context=parent_context) as span: try: operation = wrapped.__name__ - scope.span.set_tag('op', operation) - scope.span.set_tag('ep', instance._endpoint.host) - scope.span.set_tag('reg', instance._client_config.region_name) + span.set_attribute('op', operation) + span.set_attribute('ep', instance._endpoint.host) + span.set_attribute('reg', instance._client_config.region_name) - scope.span.set_tag('http.url', instance._endpoint.host + ':443/' + operation) - scope.span.set_tag('http.method', 'POST') + span.set_attribute('http.url', instance._endpoint.host + ':443/' + operation) + span.set_attribute('http.method', 'POST') arg_length = len(arg_list) if arg_length > 0: @@ -128,14 +134,14 @@ def s3_inject_method_with_instana(wrapped, instance, arg_list, kwargs): for index in range(arg_length): if fas_args[index] in ['Filename', 'Bucket', 'Key']: payload[fas_args[index]] = arg_list[index] - scope.span.set_tag('payload', payload) + span.set_attribute('payload', payload) except Exception as exc: logger.debug("s3_inject_method_with_instana: collect error", exc_info=True) try: return wrapped(*arg_list, **kwargs) except Exception as exc: - scope.span.mark_as_errored({'error': exc}) + span.mark_as_errored({'error': exc}) raise From 32d14bd8ebe614ab40c23114cb4d35f2a95bf825 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 22 Aug 2024 22:14:12 +0530 Subject: [PATCH 129/172] boto3: refactor tests Signed-off-by: Varsha GS --- src/instana/__init__.py | 2 +- tests/clients/boto3/README.md | 16 +- tests/clients/boto3/test_boto3_lambda.py | 249 +++++------ tests/clients/boto3/test_boto3_s3.py | 395 +++++++++--------- .../boto3/test_boto3_secretsmanager.py | 213 +++++----- tests/clients/boto3/test_boto3_ses.py | 203 ++++----- tests/clients/boto3/test_boto3_sqs.py | 244 +++++------ tests/conftest.py | 1 - 8 files changed, 665 insertions(+), 658 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 2ff11202..2b401a54 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -164,7 +164,7 @@ def boot_agent(): # Import & initialize instrumentation from instana.instrumentation import ( # asyncio, # noqa: F401 - # boto3_inst, # noqa: F401 + boto3_inst, # noqa: F401 # cassandra_inst, # noqa: F401 # couchbase_inst, # noqa: F401 fastapi_inst, # noqa: F401 diff --git a/tests/clients/boto3/README.md b/tests/clients/boto3/README.md index ac9fd2da..3cea338b 100644 --- a/tests/clients/boto3/README.md +++ b/tests/clients/boto3/README.md @@ -4,6 +4,8 @@ If you would like to run this test server manually from an ipython console: import os import urllib3 +from opentelemetry.semconv.trace import SpanAttributes + from moto import mock_aws import tests.apps.flask_app from tests.helpers import testenv @@ -13,12 +15,12 @@ http_client = urllib3.PoolManager() @mock_aws def test_app_boto3_sqs(): - with tracer.start_active_span('wsgi') as scope: - scope.span.set_tag('span.kind', 'entry') - scope.span.set_tag('http.host', 'localhost:80') - scope.span.set_tag('http.path', '/') - scope.span.set_tag('http.method', 'GET') - scope.span.set_tag('http.status_code', 200) - response = http_client.request('GET', testenv["wsgi_server"] + '/boto3/sqs') + with tracer.start_as_current_span("wsgi") as span: + span.set_attribute("span.kind", "entry") + span.set_attribute(SpanAttributes.HTTP_HOST, "localhost:80") + span.set_attribute("http.path", "/") + span.set_attribute(SpanAttributes.HTTP_METHOD, "GET") + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 200) + response = http_client.request("GET", testenv["wsgi_server"] + "/boto3/sqs") ``` diff --git a/tests/clients/boto3/test_boto3_lambda.py b/tests/clients/boto3/test_boto3_lambda.py index a850cbc1..153efe74 100644 --- a/tests/clients/boto3/test_boto3_lambda.py +++ b/tests/clients/boto3/test_boto3_lambda.py @@ -1,95 +1,96 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest +import pytest import json - +from typing import Generator import boto3 from moto import mock_aws from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter - -class TestLambda(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +from tests.helpers import get_first_span_by_filter + +class TestLambda: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """ Setup and Teardown """ + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws(config={"lambda": {"use_docker": False}}) self.mock.start() self.lambda_region = "us-east-1" self.aws_lambda = boto3.client('lambda', region_name=self.lambda_region) self.function_name = "myfunc" - - def tearDown(self): + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_lambda_invoke(self): - with tracer.start_active_span('test'): + def test_lambda_invoke(self) -> None: + with tracer.start_as_current_span("test"): result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert "success" == result_payload["message"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') + assert boto_span.data['boto3']['op'] == 'Invoke' endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') - - def test_lambda_invoke_as_root_exit_span(self): + assert boto_span.data['boto3']['ep'] == endpoint + assert boto_span.data['boto3']['reg'] == self.lambda_region + assert 'FunctionName' in boto_span.data['boto3']['payload'] + assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + + def test_lambda_invoke_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert "success" == result_payload["message"] spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) + assert boto_span + assert boto_span.n == "boto3" + assert boto_span.p is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') + assert boto_span.data['boto3']['op'] == 'Invoke' endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') - - def test_request_header_capture_before_call(self): + assert boto_span.data['boto3']['ep'] == endpoint + assert boto_span.data['boto3']['reg'] == self.lambda_region + assert 'FunctionName' in boto_span.data['boto3']['payload'] + assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + + def test_request_header_capture_before_call(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] @@ -108,50 +109,50 @@ def add_custom_header_before_call(params, **kwargs): # Register the function to before-call event. event_system.register('before-call.lambda.Invoke', add_custom_header_before_call) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert "success" == result_payload["message"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') + assert boto_span.data['boto3']['op'] == 'Invoke' endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') - - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert boto_span.data['boto3']['ep'] == endpoint + assert boto_span.data['boto3']['reg'] == self.lambda_region + assert 'FunctionName' in boto_span.data['boto3']['payload'] + assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self): + def test_request_header_capture_before_sign(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] @@ -171,50 +172,50 @@ def add_custom_header_before_sign(request, **kwargs): # Register the function to before-sign event. event_system.register_first('before-sign.lambda.Invoke', add_custom_header_before_sign) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert "success" == result_payload["message"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') + assert boto_span.data['boto3']['op'] == 'Invoke' endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') - - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert boto_span.data['boto3']['ep'] == endpoint + assert boto_span.data['boto3']['reg'] == self.lambda_region + assert 'FunctionName' in boto_span.data['boto3']['payload'] + assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] @@ -233,44 +234,44 @@ def modify_after_call_args(parsed, **kwargs): # Register the function to an event event_system.register('after-call.lambda.Invoke', modify_after_call_args) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) - self.assertEqual(result["StatusCode"], 200) + assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) - self.assertIn("message", result_payload) - self.assertEqual("success", result_payload["message"]) + assert "message" in result_payload + assert "success" == result_payload["message"] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'Invoke') + assert boto_span.data['boto3']['op'] == 'Invoke' endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - self.assertEqual(boto_span.data['boto3']['ep'], endpoint) - self.assertEqual(boto_span.data['boto3']['reg'], self.lambda_region) - self.assertIn('FunctionName', boto_span.data['boto3']['payload']) - self.assertEqual(boto_span.data['boto3']['payload']['FunctionName'], self.function_name) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], f'{endpoint}:443/Invoke') - - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert boto_span.data['boto3']['ep'] == endpoint + assert boto_span.data['boto3']['reg'] == self.lambda_region + assert 'FunctionName' in boto_span.data['boto3']['payload'] + assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_s3.py b/tests/clients/boto3/test_boto3_s3.py index bbffa7ec..cd45de10 100644 --- a/tests/clients/boto3/test_boto3_s3.py +++ b/tests/clients/boto3/test_boto3_s3.py @@ -2,287 +2,288 @@ # (c) Copyright Instana Inc. 2020 import os -import unittest - +import pytest +from typing import Generator from moto import mock_aws import boto3 from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter +from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) upload_filename = os.path.abspath(pwd + '/../../data/boto3/test_upload_file.jpg') download_target_filename = os.path.abspath(pwd + '/../../data/boto3/download_target_file.asdf') -class TestS3(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestS3: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """ Setup and Teardown """ + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() self.s3 = boto3.client('s3', region_name='us-east-1') - - def tearDown(self): + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_vanilla_create_bucket(self): + def test_vanilla_create_bucket(self) -> None: self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert 1 == len(result['Buckets']) + assert result['Buckets'][0]['Name'] == 'aws_bucket_name' - def test_s3_create_bucket(self): - with tracer.start_active_span('test'): + def test_s3_create_bucket(self) -> None: + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert 1 == len(result['Buckets']) + assert result['Buckets'][0]['Name'] == 'aws_bucket_name' spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') + assert boto_span.data['boto3']['op'] == 'CreateBucket' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' - def test_s3_create_bucket_as_root_exit_span(self): + def test_s3_create_bucket_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.s3.create_bucket(Bucket="aws_bucket_name") agent.options.allow_exit_as_root = False result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert 1 == len(result['Buckets']) + assert result['Buckets'][0]['Name'] == 'aws_bucket_name' spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') - - - def test_s3_list_buckets(self): - with tracer.start_active_span('test'): + assert boto_span + assert boto_span.n == "boto3" + assert boto_span.p is None + assert boto_span.ec is None + + assert boto_span.data['boto3']['op'] == 'CreateBucket' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' + + + def test_s3_list_buckets(self) -> None: + with tracer.start_as_current_span("test"): result = self.s3.list_buckets() - self.assertEqual(0, len(result['Buckets'])) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert 0 == len(result['Buckets']) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'ListBuckets') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/ListBuckets') + assert boto_span.data['boto3']['op'] == 'ListBuckets' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {} + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/ListBuckets' - def test_s3_vanilla_upload_file(self): + def test_s3_vanilla_upload_file(self) -> None: object_name = 'aws_key_name' bucket_name = 'aws_bucket_name' self.s3.create_bucket(Bucket=bucket_name) result = self.s3.upload_file(upload_filename, bucket_name, object_name) - self.assertIsNone(result) + assert result is None - def test_s3_upload_file(self): + def test_s3_upload_file(self) -> None: object_name = 'aws_key_name' bucket_name = 'aws_bucket_name' self.s3.create_bucket(Bucket=bucket_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.upload_file(upload_filename, bucket_name, object_name) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'upload_file') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'upload_file' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'Filename': upload_filename, 'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/upload_file') + assert boto_span.data['boto3']['payload'] == payload + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/upload_file' - def test_s3_upload_file_obj(self): + def test_s3_upload_file_obj(self) -> None: object_name = 'aws_key_name' bucket_name = 'aws_bucket_name' self.s3.create_bucket(Bucket=bucket_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): with open(upload_filename, "rb") as fd: self.s3.upload_fileobj(fd, bucket_name, object_name) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'upload_fileobj') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'upload_fileobj' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/upload_fileobj') + assert boto_span.data['boto3']['payload'] == payload + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/upload_fileobj' - def test_s3_download_file(self): + def test_s3_download_file(self) -> None: object_name = 'aws_key_name' bucket_name = 'aws_bucket_name' self.s3.create_bucket(Bucket=bucket_name) self.s3.upload_file(upload_filename, bucket_name, object_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.download_file(bucket_name, object_name, download_target_filename) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'download_file') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'download_file' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name', 'Filename': '%s' % download_target_filename} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/download_file') + assert boto_span.data['boto3']['payload'] == payload + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/download_file' - def test_s3_download_file_obj(self): + def test_s3_download_file_obj(self) -> None: object_name = 'aws_key_name' bucket_name = 'aws_bucket_name' self.s3.create_bucket(Bucket=bucket_name) self.s3.upload_file(upload_filename, bucket_name, object_name) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): with open(download_target_filename, "wb") as fd: self.s3.download_fileobj(bucket_name, object_name, fd) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'download_fileobj') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/download_fileobj') + assert boto_span.data['boto3']['op'] == 'download_fileobj' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/download_fileobj' - def test_request_header_capture_before_call(self): + def test_request_header_capture_before_call(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] @@ -302,47 +303,47 @@ def add_custom_header_before_call(params, **kwargs): # Register the function to before-call event. event_system.register('before-call.s3.CreateBucket', add_custom_header_before_call) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert 1 == len(result['Buckets']) + assert result['Buckets'][0]['Name'] == 'aws_bucket_name' spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') + assert boto_span.data['boto3']['op'] == 'CreateBucket' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self): + def test_request_header_capture_before_sign(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] @@ -363,47 +364,47 @@ def add_custom_header_before_sign(request, **kwargs): # Register the function to before-sign event. event_system.register_first('before-sign.s3.CreateBucket', add_custom_header_before_sign) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert 1 == len(result['Buckets']) + assert result['Buckets'][0]['Name'] == 'aws_bucket_name' spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) + assert test_span.ec is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') + assert boto_span.data['boto3']['op'] == 'CreateBucket' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] @@ -423,41 +424,41 @@ def modify_after_call_args(parsed, **kwargs): # Register the function to an event event_system.register('after-call.s3.CreateBucket', modify_after_call_args) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - self.assertEqual(1, len(result['Buckets'])) - self.assertEqual(result['Buckets'][0]['Name'], 'aws_bucket_name') + assert 1 == len(result['Buckets']) + assert result['Buckets'][0]['Name'] == 'aws_bucket_name' spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) - - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) - - self.assertIsNone(test_span.ec) - self.assertIsNone(boto_span.ec) - - self.assertEqual(boto_span.data['boto3']['op'], 'CreateBucket') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://s3.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'Bucket': 'aws_bucket_name'}) - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://s3.amazonaws.com:443/CreateBucket') - - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert boto_span + + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s + + assert test_span.ec is None + assert boto_span.ec is None + + assert boto_span.data['boto3']['op'] == 'CreateBucket' + assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' + + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_secretsmanager.py b/tests/clients/boto3/test_boto3_secretsmanager.py index 293a29c5..ca32458e 100644 --- a/tests/clients/boto3/test_boto3_secretsmanager.py +++ b/tests/clients/boto3/test_boto3_secretsmanager.py @@ -3,36 +3,37 @@ import os import boto3 -import unittest - +import pytest +from typing import Generator from moto import mock_aws from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter +from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) -class TestSecretsManager(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestSecretsManager: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """ Setup and Teardown """ + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() self.secretsmanager = boto3.client('secretsmanager', region_name='us-east-1') - - def tearDown(self): + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_vanilla_list_secrets(self): + def test_vanilla_list_secrets(self) -> None: result = self.secretsmanager.list_secrets(MaxResults=123) - self.assertListEqual(result['SecretList'], []) + assert result['SecretList'] == [] - def test_get_secret_value(self): + def test_get_secret_value(self) -> None: secret_id = 'Uber_Password' response = self.secretsmanager.create_secret( @@ -41,40 +42,40 @@ def test_get_secret_value(self): SecretString='password1', ) - self.assertEqual(response['Name'], secret_id) + assert response['Name'] == secret_id - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result['Name'] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert boto_span.data['boto3']['op'] == 'GetSecretValue' + assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert 'payload' not in boto_span.data['boto3'] - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' - def test_get_secret_value_as_root_exit_span(self): + def test_get_secret_value_as_root_exit_span(self) -> None: secret_id = 'Uber_Password' response = self.secretsmanager.create_secret( @@ -83,33 +84,33 @@ def test_get_secret_value_as_root_exit_span(self): SecretString='password1', ) - self.assertEqual(response['Name'], secret_id) + assert response['Name'] == secret_id agent.options.allow_exit_as_root = True result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result['Name'] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) + assert boto_span + assert boto_span.n == "boto3" + assert boto_span.p is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert boto_span.data['boto3']['op'] == 'GetSecretValue' + assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert 'payload' not in boto_span.data['boto3'] - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' - def test_request_header_capture_before_call(self): + def test_request_header_capture_before_call(self) -> None: secret_id = 'Uber_Password' response = self.secretsmanager.create_secret( @@ -118,7 +119,7 @@ def test_request_header_capture_before_call(self): SecretString='password1', ) - self.assertEqual(response['Name'], secret_id) + assert response['Name'] == secret_id original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] @@ -138,45 +139,45 @@ def add_custom_header_before_call(params, **kwargs): # Register the function to before-call event. event_system.register('before-call.secrets-manager.GetSecretValue', add_custom_header_before_call) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result['Name'] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert boto_span.data['boto3']['op'] == 'GetSecretValue' + assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert 'payload' not in boto_span.data['boto3'] - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self): + def test_request_header_capture_before_sign(self) -> None: secret_id = 'Uber_Password' response = self.secretsmanager.create_secret( @@ -185,7 +186,7 @@ def test_request_header_capture_before_sign(self): SecretString='password1', ) - self.assertEqual(response['Name'], secret_id) + assert response['Name'] == secret_id original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] @@ -206,45 +207,45 @@ def add_custom_header_before_sign(request, **kwargs): # Register the function to before-sign event. event_system.register_first('before-sign.secrets-manager.GetSecretValue', add_custom_header_before_sign) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result['Name'] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert boto_span.data['boto3']['op'] == 'GetSecretValue' + assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert 'payload' not in boto_span.data['boto3'] - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: secret_id = 'Uber_Password' response = self.secretsmanager.create_secret( @@ -253,7 +254,7 @@ def test_response_header_capture(self): SecretString='password1', ) - self.assertEqual(response['Name'], secret_id) + assert response['Name'] == secret_id original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] @@ -273,39 +274,39 @@ def modify_after_call_args(parsed, **kwargs): # Register the function to an event event_system.register('after-call.secrets-manager.GetSecretValue', modify_after_call_args) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - self.assertEqual(result['Name'], secret_id) + assert result['Name'] == secret_id spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'GetSecretValue') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://secretsmanager.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertNotIn('payload', boto_span.data['boto3']) + assert boto_span.data['boto3']['op'] == 'GetSecretValue' + assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert 'payload' not in boto_span.data['boto3'] - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_ses.py b/tests/clients/boto3/test_boto3_ses.py index 0e406795..a1cf9eb1 100644 --- a/tests/clients/boto3/test_boto3_ses.py +++ b/tests/clients/boto3/test_boto3_ses.py @@ -3,91 +3,92 @@ import os import boto3 -import unittest - +import pytest +from typing import Generator from moto import mock_aws from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter +from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) -class TestSes(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestSes: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """ Setup and Teardown """ + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() self.ses = boto3.client('ses', region_name='us-east-1') - - def tearDown(self): + yield # Stop Moto after each test self.mock.stop() - def test_vanilla_verify_email(self): + def test_vanilla_verify_email(self) -> None: result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 - def test_verify_email(self): - with tracer.start_active_span('test'): + def test_verify_email(self) -> None: + with tracer.start_as_current_span("test"): result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) + assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' + assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' - def test_verify_email_as_root_exit_span(self): + def test_verify_email_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) + assert boto_span + assert boto_span.n == "boto3" + assert boto_span.p is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) + assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' + assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' - def test_request_header_capture_before_call(self): + def test_request_header_capture_before_call(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] @@ -107,45 +108,45 @@ def add_custom_header_before_call(params, **kwargs): # Register the function to before-call event. event_system.register('before-call.ses.VerifyEmailIdentity', add_custom_header_before_call) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) + assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' + assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self): + def test_request_header_capture_before_sign(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] @@ -166,45 +167,45 @@ def add_custom_header_before_sign(request, **kwargs): # Register the function to before-sign event. event_system.register_first('before-sign.ses.VerifyEmailIdentity', add_custom_header_before_sign) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) + assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' + assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] @@ -224,39 +225,39 @@ def modify_after_call_args(parsed, **kwargs): # Register the function to an event event_system.register('after-call.ses.VerifyEmailIdentity', modify_after_call_args) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'VerifyEmailIdentity') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://email.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') - self.assertDictEqual(boto_span.data['boto3']['payload'], {'EmailAddress': 'pglombardo+instana299@tuta.io'}) + assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' + assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_sqs.py b/tests/clients/boto3/test_boto3_sqs.py index fc9eb57d..61e44af1 100644 --- a/tests/clients/boto3/test_boto3_sqs.py +++ b/tests/clients/boto3/test_boto3_sqs.py @@ -3,45 +3,47 @@ import os import boto3 -import unittest +import pytest import urllib3 +from typing import Generator from moto import mock_aws import tests.apps.flask_app from instana.singletons import tracer, agent -from ...helpers import get_first_span_by_filter, testenv +from tests.helpers import get_first_span_by_filter, testenv pwd = os.path.dirname(os.path.abspath(__file__)) -class TestSqs(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestSqs: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """ Setup and Teardown """ + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() self.sqs = boto3.client('sqs', region_name='us-east-1') self.http_client = urllib3.PoolManager() - - def tearDown(self): + yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_vanilla_create_queue(self): + def test_vanilla_create_queue(self) -> None: result = self.sqs.create_queue( QueueName='SQS_QUEUE_NAME', Attributes={ 'DelaySeconds': '60', 'MessageRetentionPeriod': '86400' }) - self.assertEqual(result['ResponseMetadata']['HTTPStatusCode'], 200) + assert result['ResponseMetadata']['HTTPStatusCode'] == 200 - def test_send_message(self): + def test_send_message(self) -> None: # Create the Queue: response = self.sqs.create_queue( QueueName='SQS_QUEUE_NAME', @@ -51,10 +53,10 @@ def test_send_message(self): } ) - self.assertTrue(response['QueueUrl']) + assert response['QueueUrl'] queue_url = response['QueueUrl'] - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, @@ -68,39 +70,39 @@ def test_send_message(self): 'with Instana Application Performance Monitoring') ) - self.assertTrue(response['MessageId']) + assert response['MessageId'] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'SendMessage' + assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + assert boto_span.data['boto3']['payload'] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' - def test_send_message_as_root_exit_span(self): + def test_send_message_as_root_exit_span(self) -> None: # Create the Queue: response = self.sqs.create_queue( QueueName='SQS_QUEUE_NAME', @@ -110,7 +112,7 @@ def test_send_message_as_root_exit_span(self): } ) - self.assertTrue(response['QueueUrl']) + assert response['QueueUrl'] agent.options.allow_exit_as_root = True queue_url = response['QueueUrl'] @@ -127,72 +129,72 @@ def test_send_message_as_root_exit_span(self): 'with Instana Application Performance Monitoring') ) - self.assertTrue(response['MessageId']) + assert response['MessageId'] spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) boto_span = spans[0] - self.assertTrue(boto_span) - self.assertEqual(boto_span.n, "boto3") - self.assertIsNone(boto_span.p) - self.assertIsNone(boto_span.ec) + assert boto_span + assert boto_span.n == "boto3" + assert boto_span.p is None + assert boto_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'SendMessage' + assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + assert boto_span.data['boto3']['payload'] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' - def test_app_boto3_sqs(self): - with tracer.start_active_span('test'): + def test_app_boto3_sqs(self) -> None: + with tracer.start_as_current_span("test"): self.http_client.request('GET', testenv["flask_server"] + '/boto3/sqs') spans = self.recorder.queued_spans() - self.assertEqual(5, len(spans)) + assert 5 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "urllib3" http_span = get_first_span_by_filter(spans, filter) - self.assertTrue(http_span) + assert http_span filter = lambda span: span.n == "wsgi" wsgi_span = get_first_span_by_filter(spans, filter) - self.assertTrue(wsgi_span) + assert wsgi_span filter = lambda span: span.n == "boto3" and span.data['boto3']['op'] == 'CreateQueue' bcq_span = get_first_span_by_filter(spans, filter) - self.assertTrue(bcq_span) + assert bcq_span filter = lambda span: span.n == "boto3" and span.data['boto3']['op'] == 'SendMessage' bsm_span = get_first_span_by_filter(spans, filter) - self.assertTrue(bsm_span) + assert bsm_span - self.assertEqual(http_span.t, test_span.t) - self.assertEqual(http_span.p, test_span.s) + assert http_span.t == test_span.t + assert http_span.p == test_span.s - self.assertEqual(wsgi_span.t, test_span.t) - self.assertEqual(wsgi_span.p, http_span.s) + assert wsgi_span.t == test_span.t + assert wsgi_span.p == http_span.s - self.assertEqual(bcq_span.t, test_span.t) - self.assertEqual(bcq_span.p, wsgi_span.s) + assert bcq_span.t == test_span.t + assert bcq_span.p == wsgi_span.s - self.assertEqual(bsm_span.t, test_span.t) - self.assertEqual(bsm_span.p, wsgi_span.s) + assert bsm_span.t == test_span.t + assert bsm_span.p == wsgi_span.s - def test_request_header_capture_before_call(self): + def test_request_header_capture_before_call(self) -> None: # Create the Queue: response = self.sqs.create_queue( QueueName='SQS_QUEUE_NAME', @@ -202,7 +204,7 @@ def test_request_header_capture_before_call(self): } ) - self.assertTrue(response['QueueUrl']) + assert response['QueueUrl'] original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] @@ -223,7 +225,7 @@ def add_custom_header_before_call(params, **kwargs): event_system.register('before-call.sqs.SendMessage', add_custom_header_before_call) queue_url = response['QueueUrl'] - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, @@ -237,46 +239,46 @@ def add_custom_header_before_call(params, **kwargs): 'with Instana Application Performance Monitoring') ) - self.assertTrue(response['MessageId']) + assert response['MessageId'] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'SendMessage' + assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + assert boto_span.data['boto3']['payload'] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' - self.assertIn("X-Capture-This", boto_span.data["http"]["header"]) - self.assertEqual("this", boto_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", boto_span.data["http"]["header"]) - self.assertEqual("that", boto_span.data["http"]["header"]["X-Capture-That"]) + assert "X-Capture-This" in boto_span.data["http"]["header"] + assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in boto_span.data["http"]["header"] + assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self): + def test_request_header_capture_before_sign(self) -> None: # Create the Queue: response = self.sqs.create_queue( QueueName='SQS_QUEUE_NAME', @@ -286,7 +288,7 @@ def test_request_header_capture_before_sign(self): } ) - self.assertTrue(response['QueueUrl']) + assert response['QueueUrl'] original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] @@ -308,7 +310,7 @@ def add_custom_header_before_sign(request, **kwargs): event_system.register_first('before-sign.sqs.SendMessage', add_custom_header_before_sign) queue_url = response['QueueUrl'] - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, @@ -322,46 +324,46 @@ def add_custom_header_before_sign(request, **kwargs): 'with Instana Application Performance Monitoring') ) - self.assertTrue(response['MessageId']) + assert response['MessageId'] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'SendMessage' + assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + assert boto_span.data['boto3']['payload'] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' - self.assertIn("X-Custom-1", boto_span.data["http"]["header"]) - self.assertEqual("Value1", boto_span.data["http"]["header"]["X-Custom-1"]) - self.assertIn("X-Custom-2", boto_span.data["http"]["header"]) - self.assertEqual("Value2", boto_span.data["http"]["header"]["X-Custom-2"]) + assert "X-Custom-1" in boto_span.data["http"]["header"] + assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert "X-Custom-2" in boto_span.data["http"]["header"] + assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self): + def test_response_header_capture(self) -> None: # Create the Queue: response = self.sqs.create_queue( QueueName='SQS_QUEUE_NAME', @@ -371,7 +373,7 @@ def test_response_header_capture(self): } ) - self.assertTrue(response['QueueUrl']) + assert response['QueueUrl'] original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] @@ -392,7 +394,7 @@ def modify_after_call_args(parsed, **kwargs): event_system.register('after-call.sqs.SendMessage', modify_after_call_args) queue_url = response['QueueUrl'] - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, @@ -406,40 +408,40 @@ def modify_after_call_args(parsed, **kwargs): 'with Instana Application Performance Monitoring') ) - self.assertTrue(response['MessageId']) + assert response['MessageId'] spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) - self.assertTrue(test_span) + assert test_span filter = lambda span: span.n == "boto3" boto_span = get_first_span_by_filter(spans, filter) - self.assertTrue(boto_span) + assert boto_span - self.assertEqual(boto_span.t, test_span.t) - self.assertEqual(boto_span.p, test_span.s) + assert boto_span.t == test_span.t + assert boto_span.p == test_span.s - self.assertIsNone(test_span.ec) + assert test_span.ec is None - self.assertEqual(boto_span.data['boto3']['op'], 'SendMessage') - self.assertEqual(boto_span.data['boto3']['ep'], 'https://sqs.us-east-1.amazonaws.com') - self.assertEqual(boto_span.data['boto3']['reg'], 'us-east-1') + assert boto_span.data['boto3']['op'] == 'SendMessage' + assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' + assert boto_span.data['boto3']['reg'] == 'us-east-1' payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - self.assertDictEqual(boto_span.data['boto3']['payload'], payload) + assert boto_span.data['boto3']['payload'] == payload - self.assertEqual(boto_span.data['http']['status'], 200) - self.assertEqual(boto_span.data['http']['method'], 'POST') - self.assertEqual(boto_span.data['http']['url'], 'https://sqs.us-east-1.amazonaws.com:443/SendMessage') + assert boto_span.data['http']['status'] == 200 + assert boto_span.data['http']['method'] == 'POST' + assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' - self.assertIn("X-Capture-This-Too", boto_span.data["http"]["header"]) - self.assertEqual("this too", boto_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", boto_span.data["http"]["header"]) - self.assertEqual("that too", boto_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-Capture-This-Too" in boto_span.data["http"]["header"] + assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in boto_span.data["http"]["header"] + assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/conftest.py b/tests/conftest.py index 9e6554fd..790f2ac6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,6 @@ # TODO: remove the following entries as the migration of the instrumentation # codes are finalised. -collect_ignore_glob.append("*clients/boto*") collect_ignore_glob.append("*clients/test_cassandra*") collect_ignore_glob.append("*clients/test_couchbase*") collect_ignore_glob.append("*clients/test_google*") From e824bf75c0b24c611cb64553053ef71ebb207ea8 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 30 Aug 2024 13:46:05 +0530 Subject: [PATCH 130/172] boto3: Add typehints to method signature Signed-off-by: Varsha GS --- src/instana/instrumentation/boto3_inst.py | 116 ++++++++++++++-------- tests/clients/boto3/README.md | 5 +- 2 files changed, 78 insertions(+), 43 deletions(-) diff --git a/src/instana/instrumentation/boto3_inst.py b/src/instana/instrumentation/boto3_inst.py index f5aa2b27..d832e345 100644 --- a/src/instana/instrumentation/boto3_inst.py +++ b/src/instana/instrumentation/boto3_inst.py @@ -5,6 +5,8 @@ import json import wrapt import inspect +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Sequence, Type +from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger from instana.singletons import tracer, agent @@ -12,50 +14,64 @@ from instana.propagators.format import Format from instana.span.span import get_current_span +if TYPE_CHECKING: + from instana.span.span import InstanaSpan + from botocore.auth import SigV4Auth + from botocore.client import BaseClient + try: import boto3 from boto3.s3 import inject - def extract_custom_headers(span, headers): + def extract_custom_headers(span: "InstanaSpan", headers: Dict[str, Any]) -> None: if agent.options.extra_http_headers is None or headers is None: return try: for custom_header in agent.options.extra_http_headers: if custom_header in headers: - span.set_attribute("http.header.%s" % custom_header, headers[custom_header]) + span.set_attribute( + "http.header.%s" % custom_header, headers[custom_header] + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - - def lambda_inject_context(payload, span): + def lambda_inject_context(payload: Dict[str, Any], span: "InstanaSpan") -> None: """ When boto3 lambda client 'Invoke' is called, we want to inject the tracing context. boto3/botocore has specific requirements: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda.html#Lambda.Client.invoke """ try: - invoke_payload = payload.get('Payload', {}) + invoke_payload = payload.get("Payload", {}) if not isinstance(invoke_payload, dict): invoke_payload = json.loads(invoke_payload) tracer.inject(span.context, Format.HTTP_HEADERS, invoke_payload) - payload['Payload'] = json.dumps(invoke_payload) + payload["Payload"] = json.dumps(invoke_payload) except Exception: logger.debug("non-fatal lambda_inject_context: ", exc_info=True) - @wrapt.patch_function_wrapper("botocore.auth", "SigV4Auth.add_auth") - def emit_add_auth_with_instana(wrapped, instance, args, kwargs): + def emit_add_auth_with_instana( + wrapped: Callable[..., None], + instance: "SigV4Auth", + args: Tuple[object], + kwargs: Dict[str, Any], + ) -> Callable[..., None]: current_span = get_current_span() if not tracing_is_off() and current_span and current_span.is_recording(): extract_custom_headers(current_span, args[0].headers) return wrapped(*args, **kwargs) - - @wrapt.patch_function_wrapper('botocore.client', 'BaseClient._make_api_call') - def make_api_call_with_instana(wrapped, instance, arg_list, kwargs): + @wrapt.patch_function_wrapper("botocore.client", "BaseClient._make_api_call") + def make_api_call_with_instana( + wrapped: Callable[..., Dict[str, Any]], + instance: Type["BaseClient"], + arg_list: Sequence[Dict[str, Any]], + kwargs: Dict[str, Any], + ) -> Dict[str, Any]: # If we're not tracing, just return if tracing_is_off(): return wrapped(*arg_list, **kwargs) @@ -69,50 +85,57 @@ def make_api_call_with_instana(wrapped, instance, arg_list, kwargs): operation = arg_list[0] payload = arg_list[1] - span.set_attribute('op', operation) - span.set_attribute('ep', instance._endpoint.host) - span.set_attribute('reg', instance._client_config.region_name) + span.set_attribute("op", operation) + span.set_attribute("ep", instance._endpoint.host) + span.set_attribute("reg", instance._client_config.region_name) - span.set_attribute('http.url', instance._endpoint.host + ':443/' + arg_list[0]) - span.set_attribute('http.method', 'POST') + span.set_attribute( + SpanAttributes.HTTP_URL, + instance._endpoint.host + ":443/" + arg_list[0], + ) + span.set_attribute(SpanAttributes.HTTP_METHOD, "POST") # Don't collect payload for SecretsManager - if not hasattr(instance, 'get_secret_value'): - span.set_attribute('payload', payload) + if not hasattr(instance, "get_secret_value"): + span.set_attribute("payload", payload) # Inject context when invoking lambdas - if 'lambda' in instance._endpoint.host and operation == 'Invoke': + if "lambda" in instance._endpoint.host and operation == "Invoke": lambda_inject_context(payload, span) - except Exception as exc: + except Exception: logger.debug("make_api_call_with_instana: collect error", exc_info=True) try: result = wrapped(*arg_list, **kwargs) if isinstance(result, dict): - http_dict = result.get('ResponseMetadata') + http_dict = result.get("ResponseMetadata") if isinstance(http_dict, dict): - status = http_dict.get('HTTPStatusCode') + status = http_dict.get("HTTPStatusCode") if status is not None: - span.set_attribute('http.status_code', status) - headers = http_dict.get('HTTPHeaders') + span.set_attribute("http.status_code", status) + headers = http_dict.get("HTTPHeaders") extract_custom_headers(span, headers) return result except Exception as exc: - span.mark_as_errored({'error': exc}) + span.mark_as_errored({"error": exc}) raise - - def s3_inject_method_with_instana(wrapped, instance, arg_list, kwargs): + def s3_inject_method_with_instana( + wrapped: Callable[..., object], + instance: Type["BaseClient"], + arg_list: Sequence[object], + kwargs: Dict[str, Any], + ) -> Callable[..., object]: # If we're not tracing, just return if tracing_is_off(): return wrapped(*arg_list, **kwargs) fas = inspect.getfullargspec(wrapped) fas_args = fas.args - fas_args.remove('self') + fas_args.remove("self") tracer, parent_span, _ = get_tracer_tuple() @@ -121,32 +144,43 @@ def s3_inject_method_with_instana(wrapped, instance, arg_list, kwargs): with tracer.start_as_current_span("boto3", span_context=parent_context) as span: try: operation = wrapped.__name__ - span.set_attribute('op', operation) - span.set_attribute('ep', instance._endpoint.host) - span.set_attribute('reg', instance._client_config.region_name) + span.set_attribute("op", operation) + span.set_attribute("ep", instance._endpoint.host) + span.set_attribute("reg", instance._client_config.region_name) - span.set_attribute('http.url', instance._endpoint.host + ':443/' + operation) - span.set_attribute('http.method', 'POST') + span.set_attribute( + SpanAttributes.HTTP_URL, + instance._endpoint.host + ":443/" + operation, + ) + span.set_attribute(SpanAttributes.HTTP_METHOD, "POST") arg_length = len(arg_list) if arg_length > 0: payload = {} for index in range(arg_length): - if fas_args[index] in ['Filename', 'Bucket', 'Key']: + if fas_args[index] in ["Filename", "Bucket", "Key"]: payload[fas_args[index]] = arg_list[index] - span.set_attribute('payload', payload) - except Exception as exc: - logger.debug("s3_inject_method_with_instana: collect error", exc_info=True) + span.set_attribute("payload", payload) + except Exception: + logger.debug( + "s3_inject_method_with_instana: collect error", exc_info=True + ) try: return wrapped(*arg_list, **kwargs) except Exception as exc: - span.mark_as_errored({'error': exc}) + span.mark_as_errored({"error": exc}) raise - - for method in ['upload_file', 'upload_fileobj', 'download_file', 'download_fileobj']: - wrapt.wrap_function_wrapper('boto3.s3.inject', method, s3_inject_method_with_instana) + for method in [ + "upload_file", + "upload_fileobj", + "download_file", + "download_fileobj", + ]: + wrapt.wrap_function_wrapper( + "boto3.s3.inject", method, s3_inject_method_with_instana + ) logger.debug("Instrumenting boto3") except ImportError: diff --git a/tests/clients/boto3/README.md b/tests/clients/boto3/README.md index 3cea338b..33c9a199 100644 --- a/tests/clients/boto3/README.md +++ b/tests/clients/boto3/README.md @@ -5,6 +5,7 @@ import os import urllib3 from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind from moto import mock_aws import tests.apps.flask_app @@ -15,8 +16,8 @@ http_client = urllib3.PoolManager() @mock_aws def test_app_boto3_sqs(): - with tracer.start_as_current_span("wsgi") as span: - span.set_attribute("span.kind", "entry") + with tracer.start_as_current_span("test") as span: + span.set_attribute("span.kind", SpanKind.SERVER) span.set_attribute(SpanAttributes.HTTP_HOST, "localhost:80") span.set_attribute("http.path", "/") span.set_attribute(SpanAttributes.HTTP_METHOD, "GET") From 8255cf441d922f30972b8f77d5277bbab9d9b190 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 30 Aug 2024 13:46:26 +0530 Subject: [PATCH 131/172] boto3: Add style changes Signed-off-by: Varsha GS --- src/instana/instrumentation/boto3_inst.py | 8 +- tests/clients/boto3/test_boto3_lambda.py | 205 +++++----- tests/clients/boto3/test_boto3_s3.py | 346 ++++++++-------- .../boto3/test_boto3_secretsmanager.py | 239 ++++++----- tests/clients/boto3/test_boto3_ses.py | 213 +++++----- tests/clients/boto3/test_boto3_sqs.py | 382 ++++++++++-------- 6 files changed, 759 insertions(+), 634 deletions(-) diff --git a/src/instana/instrumentation/boto3_inst.py b/src/instana/instrumentation/boto3_inst.py index d832e345..fb7a3233 100644 --- a/src/instana/instrumentation/boto3_inst.py +++ b/src/instana/instrumentation/boto3_inst.py @@ -5,7 +5,7 @@ import json import wrapt import inspect -from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Sequence, Type +from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Sequence, Type, Optional from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger @@ -23,8 +23,10 @@ import boto3 from boto3.s3 import inject - def extract_custom_headers(span: "InstanaSpan", headers: Dict[str, Any]) -> None: - if agent.options.extra_http_headers is None or headers is None: + def extract_custom_headers( + span: "InstanaSpan", headers: Optional[Dict[str, Any]] = None + ) -> None: + if not agent.options.extra_http_headers or not headers: return try: for custom_header in agent.options.extra_http_headers: diff --git a/tests/clients/boto3/test_boto3_lambda.py b/tests/clients/boto3/test_boto3_lambda.py index 153efe74..78117804 100644 --- a/tests/clients/boto3/test_boto3_lambda.py +++ b/tests/clients/boto3/test_boto3_lambda.py @@ -10,17 +10,18 @@ from instana.singletons import tracer, agent from tests.helpers import get_first_span_by_filter + class TestLambda: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Setup and Teardown """ + """Setup and Teardown""" # Clear all spans before a test run self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws(config={"lambda": {"use_docker": False}}) self.mock.start() self.lambda_region = "us-east-1" - self.aws_lambda = boto3.client('lambda', region_name=self.lambda_region) + self.aws_lambda = boto3.client("lambda", region_name=self.lambda_region) self.function_name = "myfunc" yield # Stop Moto after each test @@ -29,15 +30,18 @@ def _resource(self) -> Generator[None, None, None]: def test_lambda_invoke(self) -> None: with tracer.start_as_current_span("test"): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) assert "message" in result_payload - assert "success" == result_payload["message"] + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -50,75 +54,79 @@ def test_lambda_invoke(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None + assert not test_span.ec + assert not boto_span.ec - assert boto_span.data['boto3']['op'] == 'Invoke' - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - assert boto_span.data['boto3']['ep'] == endpoint - assert boto_span.data['boto3']['reg'] == self.lambda_region - assert 'FunctionName' in boto_span.data['boto3']['payload'] - assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" def test_lambda_invoke_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + result = self.aws_lambda.invoke( + FunctionName=self.function_name, Payload=json.dumps({"message": "success"}) + ) assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) assert "message" in result_payload - assert "success" == result_payload["message"] + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - assert 1 == len(spans) + assert len(spans) == 1 boto_span = spans[0] assert boto_span assert boto_span.n == "boto3" - assert boto_span.p is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'Invoke' - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - assert boto_span.data['boto3']['ep'] == endpoint - assert boto_span.data['boto3']['reg'] == self.lambda_region - assert 'FunctionName' in boto_span.data['boto3']['payload'] - assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" def test_request_header_capture_before_call(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.aws_lambda.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.lambda.Invoke', add_custom_header_before_call) + event_system.register( + "before-call.lambda.Invoke", add_custom_header_before_call + ) with tracer.start_as_current_span("test"): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) assert "message" in result_payload - assert "success" == result_payload["message"] + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -131,38 +139,34 @@ def add_custom_header_before_call(params, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None + assert not test_span.ec + assert not boto_span.ec - assert boto_span.data['boto3']['op'] == 'Invoke' - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - assert boto_span.data['boto3']['ep'] == endpoint - assert boto_span.data['boto3']['reg'] == self.lambda_region - assert 'FunctionName' in boto_span.data['boto3']['payload'] - assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" assert "X-Capture-This" in boto_span.data["http"]["header"] - assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" assert "X-Capture-That" in boto_span.data["http"]["header"] - assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.aws_lambda.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -170,18 +174,23 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.lambda.Invoke', add_custom_header_before_sign) + event_system.register_first( + "before-sign.lambda.Invoke", add_custom_header_before_sign + ) with tracer.start_as_current_span("test"): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) assert "message" in result_payload - assert "success" == result_payload["message"] + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -194,30 +203,29 @@ def add_custom_header_before_sign(request, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None + assert not test_span.ec + assert not boto_span.ec - assert boto_span.data['boto3']['op'] == 'Invoke' - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - assert boto_span.data['boto3']['ep'] == endpoint - assert boto_span.data['boto3']['reg'] == self.lambda_region - assert 'FunctionName' in boto_span.data['boto3']['payload'] - assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" assert "X-Custom-1" in boto_span.data["http"]["header"] - assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" assert "X-Custom-2" in boto_span.data["http"]["header"] - assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.aws_lambda.meta.events @@ -229,21 +237,24 @@ def test_response_header_capture(self) -> None: # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.lambda.Invoke', modify_after_call_args) + event_system.register("after-call.lambda.Invoke", modify_after_call_args) with tracer.start_as_current_span("test"): - result = self.aws_lambda.invoke(FunctionName=self.function_name, Payload=json.dumps({"message": "success"})) + result = self.aws_lambda.invoke( + FunctionName=self.function_name, + Payload=json.dumps({"message": "success"}), + ) assert result["StatusCode"] == 200 result_payload = json.loads(result["Payload"].read().decode("utf-8")) assert "message" in result_payload - assert "success" == result_payload["message"] + assert result_payload["message"] == "success" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -256,22 +267,22 @@ def modify_after_call_args(parsed, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None + assert not test_span.ec + assert not boto_span.ec - assert boto_span.data['boto3']['op'] == 'Invoke' - endpoint = f'https://lambda.{self.lambda_region}.amazonaws.com' - assert boto_span.data['boto3']['ep'] == endpoint - assert boto_span.data['boto3']['reg'] == self.lambda_region - assert 'FunctionName' in boto_span.data['boto3']['payload'] - assert boto_span.data['boto3']['payload']['FunctionName'] == self.function_name - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == f'{endpoint}:443/Invoke' + assert boto_span.data["boto3"]["op"] == "Invoke" + endpoint = f"https://lambda.{self.lambda_region}.amazonaws.com" + assert boto_span.data["boto3"]["ep"] == endpoint + assert boto_span.data["boto3"]["reg"] == self.lambda_region + assert "FunctionName" in boto_span.data["boto3"]["payload"] + assert boto_span.data["boto3"]["payload"]["FunctionName"] == self.function_name + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert boto_span.data["http"]["url"] == f"{endpoint}:443/Invoke" assert "X-Capture-This-Too" in boto_span.data["http"]["header"] - assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" assert "X-Capture-That-Too" in boto_span.data["http"]["header"] - assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_s3.py b/tests/clients/boto3/test_boto3_s3.py index cd45de10..6410a6ea 100644 --- a/tests/clients/boto3/test_boto3_s3.py +++ b/tests/clients/boto3/test_boto3_s3.py @@ -11,44 +11,44 @@ from tests.helpers import get_first_span_by_filter pwd = os.path.dirname(os.path.abspath(__file__)) -upload_filename = os.path.abspath(pwd + '/../../data/boto3/test_upload_file.jpg') -download_target_filename = os.path.abspath(pwd + '/../../data/boto3/download_target_file.asdf') +upload_filename = os.path.abspath(pwd + "/../../data/boto3/test_upload_file.jpg") +download_target_filename = os.path.abspath( + pwd + "/../../data/boto3/download_target_file.asdf" +) class TestS3: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Setup and Teardown """ + """Setup and Teardown""" # Clear all spans before a test run self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.s3 = boto3.client('s3', region_name='us-east-1') + self.s3 = boto3.client("s3", region_name="us-east-1") yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_vanilla_create_bucket(self) -> None: self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - assert 1 == len(result['Buckets']) - assert result['Buckets'][0]['Name'] == 'aws_bucket_name' - + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" def test_s3_create_bucket(self) -> None: with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - assert 1 == len(result['Buckets']) - assert result['Buckets'][0]['Name'] == 'aws_bucket_name' + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -61,17 +61,18 @@ def test_s3_create_bucket(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'CreateBucket' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) def test_s3_create_bucket_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True @@ -79,35 +80,36 @@ def test_s3_create_bucket_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = False result = self.s3.list_buckets() - assert 1 == len(result['Buckets']) - assert result['Buckets'][0]['Name'] == 'aws_bucket_name' + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - assert 1 == len(spans) + assert len(spans) == 1 boto_span = spans[0] assert boto_span assert boto_span.n == "boto3" - assert boto_span.p is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'CreateBucket' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' - + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) def test_s3_list_buckets(self) -> None: with tracer.start_as_current_span("test"): result = self.s3.list_buckets() - assert 0 == len(result['Buckets']) - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 + assert len(result["Buckets"]) == 0 + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -120,30 +122,30 @@ def test_s3_list_buckets(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'ListBuckets' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {} - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/ListBuckets' + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "ListBuckets" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/ListBuckets" + ) def test_s3_vanilla_upload_file(self) -> None: - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) result = self.s3.upload_file(upload_filename, bucket_name, object_name) - assert result is None - + assert not result def test_s3_upload_file(self) -> None: - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) @@ -151,7 +153,7 @@ def test_s3_upload_file(self) -> None: self.s3.upload_file(upload_filename, bucket_name, object_name) spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -164,21 +166,26 @@ def test_s3_upload_file(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'upload_file' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - payload = {'Filename': upload_filename, 'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name'} - assert boto_span.data['boto3']['payload'] == payload - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/upload_file' + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "upload_file" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + payload = { + "Filename": upload_filename, + "Bucket": "aws_bucket_name", + "Key": "aws_key_name", + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/upload_file" + ) def test_s3_upload_file_obj(self) -> None: - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) @@ -187,7 +194,7 @@ def test_s3_upload_file_obj(self) -> None: self.s3.upload_fileobj(fd, bucket_name, object_name) spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -200,21 +207,23 @@ def test_s3_upload_file_obj(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'upload_fileobj' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - payload = {'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name'} - assert boto_span.data['boto3']['payload'] == payload - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/upload_fileobj' + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "upload_fileobj" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + payload = {"Bucket": "aws_bucket_name", "Key": "aws_key_name"} + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://s3.amazonaws.com:443/upload_fileobj" + ) def test_s3_download_file(self) -> None: - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) self.s3.upload_file(upload_filename, bucket_name, object_name) @@ -223,7 +232,7 @@ def test_s3_download_file(self) -> None: self.s3.download_file(bucket_name, object_name, download_target_filename) spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -236,21 +245,27 @@ def test_s3_download_file(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'download_file' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - payload = {'Bucket': 'aws_bucket_name', 'Key': 'aws_key_name', 'Filename': '%s' % download_target_filename} - assert boto_span.data['boto3']['payload'] == payload - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/download_file' + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "download_file" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + payload = { + "Bucket": "aws_bucket_name", + "Key": "aws_key_name", + "Filename": "%s" % download_target_filename, + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://s3.amazonaws.com:443/download_file" + ) def test_s3_download_file_obj(self) -> None: - object_name = 'aws_key_name' - bucket_name = 'aws_bucket_name' + object_name = "aws_key_name" + bucket_name = "aws_bucket_name" self.s3.create_bucket(Bucket=bucket_name) self.s3.upload_file(upload_filename, bucket_name, object_name) @@ -260,7 +275,7 @@ def test_s3_download_file_obj(self) -> None: self.s3.download_fileobj(bucket_name, object_name, fd) spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -273,45 +288,45 @@ def test_s3_download_file_obj(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'download_fileobj' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/download_fileobj' + assert not test_span.ec + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "download_fileobj" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://s3.amazonaws.com:443/download_fileobj" + ) def test_request_header_capture_before_call(self) -> None: - original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.s3.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.s3.CreateBucket', add_custom_header_before_call) + event_system.register( + "before-call.s3.CreateBucket", add_custom_header_before_call + ) with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - assert 1 == len(result['Buckets']) - assert result['Buckets'][0]['Name'] == 'aws_bucket_name' + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -324,37 +339,34 @@ def add_custom_header_before_call(params, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None + assert not test_span.ec + assert not boto_span.ec - assert boto_span.data['boto3']['op'] == 'CreateBucket' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) assert "X-Capture-This" in boto_span.data["http"]["header"] - assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" assert "X-Capture-That" in boto_span.data["http"]["header"] - assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self) -> None: - original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.s3.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -362,17 +374,19 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.s3.CreateBucket', add_custom_header_before_sign) + event_system.register_first( + "before-sign.s3.CreateBucket", add_custom_header_before_sign + ) with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - assert 1 == len(result['Buckets']) - assert result['Buckets'][0]['Name'] == 'aws_bucket_name' + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -385,29 +399,29 @@ def add_custom_header_before_sign(request, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None + assert not test_span.ec + assert not boto_span.ec - assert boto_span.data['boto3']['op'] == 'CreateBucket' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) assert "X-Custom-1" in boto_span.data["http"]["header"] - assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" assert "X-Custom-2" in boto_span.data["http"]["header"] - assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self) -> None: - original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.s3.meta.events @@ -419,20 +433,20 @@ def test_response_header_capture(self) -> None: # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.s3.CreateBucket', modify_after_call_args) + event_system.register("after-call.s3.CreateBucket", modify_after_call_args) with tracer.start_as_current_span("test"): self.s3.create_bucket(Bucket="aws_bucket_name") result = self.s3.list_buckets() - assert 1 == len(result['Buckets']) - assert result['Buckets'][0]['Name'] == 'aws_bucket_name' + assert len(result["Buckets"]) == 1 + assert result["Buckets"][0]["Name"] == "aws_bucket_name" spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -445,20 +459,22 @@ def modify_after_call_args(parsed, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - assert boto_span.ec is None + assert not test_span.ec + assert not boto_span.ec - assert boto_span.data['boto3']['op'] == 'CreateBucket' - assert boto_span.data['boto3']['ep'] == 'https://s3.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'Bucket': 'aws_bucket_name'} - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://s3.amazonaws.com:443/CreateBucket' + assert boto_span.data["boto3"]["op"] == "CreateBucket" + assert boto_span.data["boto3"]["ep"] == "https://s3.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == {"Bucket": "aws_bucket_name"} + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] == "https://s3.amazonaws.com:443/CreateBucket" + ) assert "X-Capture-This-Too" in boto_span.data["http"]["header"] - assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" assert "X-Capture-That-Too" in boto_span.data["http"]["header"] - assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_secretsmanager.py b/tests/clients/boto3/test_boto3_secretsmanager.py index ca32458e..e8a715fc 100644 --- a/tests/clients/boto3/test_boto3_secretsmanager.py +++ b/tests/clients/boto3/test_boto3_secretsmanager.py @@ -12,45 +12,44 @@ pwd = os.path.dirname(os.path.abspath(__file__)) + class TestSecretsManager: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Setup and Teardown """ + """Setup and Teardown""" # Clear all spans before a test run self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.secretsmanager = boto3.client('secretsmanager', region_name='us-east-1') + self.secretsmanager = boto3.client("secretsmanager", region_name="us-east-1") yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_vanilla_list_secrets(self) -> None: result = self.secretsmanager.list_secrets(MaxResults=123) - assert result['SecretList'] == [] - + assert result["SecretList"] == [] def test_get_secret_value(self) -> None: - secret_id = 'Uber_Password' + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - assert response['Name'] == secret_id + assert response["Name"] == secret_id with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - assert result['Name'] == secret_id + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -63,89 +62,98 @@ def test_get_secret_value(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - - assert boto_span.data['boto3']['op'] == 'GetSecretValue' - assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert 'payload' not in boto_span.data['boto3'] - - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' + assert not test_span.ec + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) def test_get_secret_value_as_root_exit_span(self) -> None: - secret_id = 'Uber_Password' + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - assert response['Name'] == secret_id + assert response["Name"] == secret_id agent.options.allow_exit_as_root = True result = self.secretsmanager.get_secret_value(SecretId=secret_id) - assert result['Name'] == secret_id + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - assert 1 == len(spans) + assert len(spans) == 1 boto_span = spans[0] assert boto_span assert boto_span.n == "boto3" - assert boto_span.p is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'GetSecretValue' - assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert 'payload' not in boto_span.data['boto3'] - - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' + assert not boto_span.p + assert not boto_span.ec + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) def test_request_header_capture_before_call(self) -> None: - secret_id = 'Uber_Password' + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - assert response['Name'] == secret_id + assert response["Name"] == secret_id original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.secretsmanager.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.secrets-manager.GetSecretValue', add_custom_header_before_call) + event_system.register( + "before-call.secrets-manager.GetSecretValue", add_custom_header_before_call + ) with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - assert result['Name'] == secret_id + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -158,46 +166,48 @@ def add_custom_header_before_call(params, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - - assert boto_span.data['boto3']['op'] == 'GetSecretValue' - assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert 'payload' not in boto_span.data['boto3'] + assert not test_span.ec - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) assert "X-Capture-This" in boto_span.data["http"]["header"] - assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" assert "X-Capture-That" in boto_span.data["http"]["header"] - assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self) -> None: - secret_id = 'Uber_Password' + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - assert response['Name'] == secret_id + assert response["Name"] == secret_id original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.secretsmanager.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -205,15 +215,17 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.secrets-manager.GetSecretValue', add_custom_header_before_sign) + event_system.register_first( + "before-sign.secrets-manager.GetSecretValue", add_custom_header_before_sign + ) with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - assert result['Name'] == secret_id + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -226,38 +238,43 @@ def add_custom_header_before_sign(request, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - - assert boto_span.data['boto3']['op'] == 'GetSecretValue' - assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert 'payload' not in boto_span.data['boto3'] + assert not test_span.ec - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) assert "X-Custom-1" in boto_span.data["http"]["header"] - assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" assert "X-Custom-2" in boto_span.data["http"]["header"] - assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self) -> None: - secret_id = 'Uber_Password' + secret_id = "Uber_Password" response = self.secretsmanager.create_secret( Name=secret_id, - SecretBinary=b'password1', - SecretString='password1', + SecretBinary=b"password1", + SecretString="password1", ) - assert response['Name'] == secret_id + assert response["Name"] == secret_id original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.secretsmanager.meta.events @@ -269,18 +286,20 @@ def test_response_header_capture(self) -> None: # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.secrets-manager.GetSecretValue', modify_after_call_args) + event_system.register( + "after-call.secrets-manager.GetSecretValue", modify_after_call_args + ) with tracer.start_as_current_span("test"): result = self.secretsmanager.get_secret_value(SecretId=secret_id) - assert result['Name'] == secret_id + assert result["Name"] == secret_id spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -293,20 +312,26 @@ def modify_after_call_args(parsed, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None + assert not test_span.ec - assert boto_span.data['boto3']['op'] == 'GetSecretValue' - assert boto_span.data['boto3']['ep'] == 'https://secretsmanager.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert 'payload' not in boto_span.data['boto3'] - - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue' + assert boto_span.data["boto3"]["op"] == "GetSecretValue" + assert ( + boto_span.data["boto3"]["ep"] + == "https://secretsmanager.us-east-1.amazonaws.com" + ) + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert "payload" not in boto_span.data["boto3"] + + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://secretsmanager.us-east-1.amazonaws.com:443/GetSecretValue" + ) assert "X-Capture-This-Too" in boto_span.data["http"]["header"] - assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" assert "X-Capture-That-Too" in boto_span.data["http"]["header"] - assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_ses.py b/tests/clients/boto3/test_boto3_ses.py index a1cf9eb1..afea6b0e 100644 --- a/tests/clients/boto3/test_boto3_ses.py +++ b/tests/clients/boto3/test_boto3_ses.py @@ -12,34 +12,37 @@ pwd = os.path.dirname(os.path.abspath(__file__)) + class TestSes: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Setup and Teardown """ + """Setup and Teardown""" # Clear all spans before a test run self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.ses = boto3.client('ses', region_name='us-east-1') + self.ses = boto3.client("ses", region_name="us-east-1") yield # Stop Moto after each test self.mock.stop() - def test_vanilla_verify_email(self) -> None: - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 - + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 def test_verify_email(self) -> None: with tracer.start_as_current_span("test"): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -52,69 +55,79 @@ def test_verify_email(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - - assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' - assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} + assert not test_span.ec - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) def test_verify_email_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - assert 1 == len(spans) + assert len(spans) == 1 boto_span = spans[0] assert boto_span assert boto_span.n == "boto3" - assert boto_span.p is None - assert boto_span.ec is None - - assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' - assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} - - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) def test_request_header_capture_before_call(self) -> None: - original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.ses.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.ses.VerifyEmailIdentity', add_custom_header_before_call) + event_system.register( + "before-call.ses.VerifyEmailIdentity", add_custom_header_before_call + ) with tracer.start_as_current_span("test"): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -127,37 +140,37 @@ def add_custom_header_before_call(params, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None + assert not test_span.ec - assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' - assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) assert "X-Capture-This" in boto_span.data["http"]["header"] - assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" assert "X-Capture-That" in boto_span.data["http"]["header"] - assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self) -> None: - original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.ses.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -165,15 +178,19 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.ses.VerifyEmailIdentity', add_custom_header_before_sign) + event_system.register_first( + "before-sign.ses.VerifyEmailIdentity", add_custom_header_before_sign + ) with tracer.start_as_current_span("test"): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -186,29 +203,32 @@ def add_custom_header_before_sign(request, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None + assert not test_span.ec - assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' - assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) assert "X-Custom-1" in boto_span.data["http"]["header"] - assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" assert "X-Custom-2" in boto_span.data["http"]["header"] - assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self) -> None: - original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.ses.meta.events @@ -220,18 +240,22 @@ def test_response_header_capture(self) -> None: # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.ses.VerifyEmailIdentity', modify_after_call_args) + event_system.register( + "after-call.ses.VerifyEmailIdentity", modify_after_call_args + ) with tracer.start_as_current_span("test"): - result = self.ses.verify_email_identity(EmailAddress='pglombardo+instana299@tuta.io') + result = self.ses.verify_email_identity( + EmailAddress="pglombardo+instana299@tuta.io" + ) - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -244,20 +268,25 @@ def modify_after_call_args(parsed, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None + assert not test_span.ec - assert boto_span.data['boto3']['op'] == 'VerifyEmailIdentity' - assert boto_span.data['boto3']['ep'] == 'https://email.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - assert boto_span.data['boto3']['payload'] == {'EmailAddress': 'pglombardo+instana299@tuta.io'} + assert boto_span.data["boto3"]["op"] == "VerifyEmailIdentity" + assert boto_span.data["boto3"]["ep"] == "https://email.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + assert boto_span.data["boto3"]["payload"] == { + "EmailAddress": "pglombardo+instana299@tuta.io" + } - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity' + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://email.us-east-1.amazonaws.com:443/VerifyEmailIdentity" + ) assert "X-Capture-This-Too" in boto_span.data["http"]["header"] - assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" assert "X-Capture-That-Too" in boto_span.data["http"]["header"] - assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/clients/boto3/test_boto3_sqs.py b/tests/clients/boto3/test_boto3_sqs.py index 61e44af1..e7755b1b 100644 --- a/tests/clients/boto3/test_boto3_sqs.py +++ b/tests/clients/boto3/test_boto3_sqs.py @@ -19,61 +19,56 @@ class TestSqs: @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: - """ Setup and Teardown """ + """Setup and Teardown""" # Clear all spans before a test run self.recorder = tracer.span_processor self.recorder.clear_spans() self.mock = mock_aws() self.mock.start() - self.sqs = boto3.client('sqs', region_name='us-east-1') + self.sqs = boto3.client("sqs", region_name="us-east-1") self.http_client = urllib3.PoolManager() yield # Stop Moto after each test self.mock.stop() agent.options.allow_exit_as_root = False - def test_vanilla_create_queue(self) -> None: result = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '86400' - }) - assert result['ResponseMetadata']['HTTPStatusCode'] == 200 - + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "86400"}, + ) + assert result["ResponseMetadata"]["HTTPStatusCode"] == 200 def test_send_message(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - assert response['QueueUrl'] - queue_url = response['QueueUrl'] + assert response["QueueUrl"] + queue_url = response["QueueUrl"] with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - assert response['MessageId'] + assert response["MessageId"] spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -86,80 +81,98 @@ def test_send_message(self) -> None: assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None - - assert boto_span.data['boto3']['op'] == 'SendMessage' - assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert not test_span.ec - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - assert boto_span.data['boto3']['payload'] == payload + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) def test_send_message_as_root_exit_span(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - assert response['QueueUrl'] + assert response["QueueUrl"] agent.options.allow_exit_as_root = True - queue_url = response['QueueUrl'] + queue_url = response["QueueUrl"] response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - assert response['MessageId'] + assert response["MessageId"] spans = self.recorder.queued_spans() - assert 1 == len(spans) + assert len(spans) == 1 boto_span = spans[0] assert boto_span assert boto_span.n == "boto3" - assert boto_span.p is None - assert boto_span.ec is None - - - assert boto_span.data['boto3']['op'] == 'SendMessage' - assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' - - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - assert boto_span.data['boto3']['payload'] == payload - - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' + assert not boto_span.p + assert not boto_span.ec + + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" + + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) def test_app_boto3_sqs(self) -> None: with tracer.start_as_current_span("test"): - self.http_client.request('GET', testenv["flask_server"] + '/boto3/sqs') + self.http_client.request("GET", testenv["flask_server"] + "/boto3/sqs") spans = self.recorder.queued_spans() - assert 5 == len(spans) + assert len(spans) == 5 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -173,11 +186,15 @@ def test_app_boto3_sqs(self) -> None: wsgi_span = get_first_span_by_filter(spans, filter) assert wsgi_span - filter = lambda span: span.n == "boto3" and span.data['boto3']['op'] == 'CreateQueue' + filter = ( + lambda span: span.n == "boto3" and span.data["boto3"]["op"] == "CreateQueue" + ) bcq_span = get_first_span_by_filter(spans, filter) assert bcq_span - filter = lambda span: span.n == "boto3" and span.data['boto3']['op'] == 'SendMessage' + filter = ( + lambda span: span.n == "boto3" and span.data["boto3"]["op"] == "SendMessage" + ) bsm_span = get_first_span_by_filter(spans, filter) assert bsm_span @@ -193,56 +210,53 @@ def test_app_boto3_sqs(self) -> None: assert bsm_span.t == test_span.t assert bsm_span.p == wsgi_span.s - def test_request_header_capture_before_call(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - assert response['QueueUrl'] + assert response["QueueUrl"] original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This', 'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] # Access the event system on the S3 client event_system = self.sqs.meta.events - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} # Create a function that adds custom headers def add_custom_header_before_call(params, **kwargs): - params['headers'].update(request_headers) + params["headers"].update(request_headers) # Register the function to before-call event. - event_system.register('before-call.sqs.SendMessage', add_custom_header_before_call) + event_system.register( + "before-call.sqs.SendMessage", add_custom_header_before_call + ) - queue_url = response['QueueUrl'] + queue_url = response["QueueUrl"] with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - assert response['MessageId'] + assert response["MessageId"] spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -255,51 +269,55 @@ def add_custom_header_before_call(params, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None + assert not test_span.ec - assert boto_span.data['boto3']['op'] == 'SendMessage' - assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - assert boto_span.data['boto3']['payload'] == payload + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) assert "X-Capture-This" in boto_span.data["http"]["header"] - assert "this" == boto_span.data["http"]["header"]["X-Capture-This"] + assert boto_span.data["http"]["header"]["X-Capture-This"] == "this" assert "X-Capture-That" in boto_span.data["http"]["header"] - assert "that" == boto_span.data["http"]["header"]["X-Capture-That"] + assert boto_span.data["http"]["header"]["X-Capture-That"] == "that" agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture_before_sign(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - assert response['QueueUrl'] + assert response["QueueUrl"] original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Custom-1', 'X-Custom-2'] + agent.options.extra_http_headers = ["X-Custom-1", "X-Custom-2"] # Access the event system on the S3 client event_system = self.sqs.meta.events - request_headers = { - 'X-Custom-1': 'Value1', - 'X-Custom-2': 'Value2' - } + request_headers = {"X-Custom-1": "Value1", "X-Custom-2": "Value2"} # Create a function that adds custom headers def add_custom_header_before_sign(request, **kwargs): @@ -307,27 +325,31 @@ def add_custom_header_before_sign(request, **kwargs): request.headers.add_header(name, value) # Register the function to before-sign event. - event_system.register_first('before-sign.sqs.SendMessage', add_custom_header_before_sign) + event_system.register_first( + "before-sign.sqs.SendMessage", add_custom_header_before_sign + ) - queue_url = response['QueueUrl'] + queue_url = response["QueueUrl"] with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - assert response['MessageId'] + assert response["MessageId"] spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -340,43 +362,50 @@ def add_custom_header_before_sign(request, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None + assert not test_span.ec - assert boto_span.data['boto3']['op'] == 'SendMessage' - assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - assert boto_span.data['boto3']['payload'] == payload + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) assert "X-Custom-1" in boto_span.data["http"]["header"] - assert "Value1" == boto_span.data["http"]["header"]["X-Custom-1"] + assert boto_span.data["http"]["header"]["X-Custom-1"] == "Value1" assert "X-Custom-2" in boto_span.data["http"]["header"] - assert "Value2" == boto_span.data["http"]["header"]["X-Custom-2"] + assert boto_span.data["http"]["header"]["X-Custom-2"] == "Value2" agent.options.extra_http_headers = original_extra_http_headers - def test_response_header_capture(self) -> None: # Create the Queue: response = self.sqs.create_queue( - QueueName='SQS_QUEUE_NAME', - Attributes={ - 'DelaySeconds': '60', - 'MessageRetentionPeriod': '600' - } + QueueName="SQS_QUEUE_NAME", + Attributes={"DelaySeconds": "60", "MessageRetentionPeriod": "600"}, ) - assert response['QueueUrl'] + assert response["QueueUrl"] original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This-Too', 'X-Capture-That-Too'] + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] # Access the event system on the S3 client event_system = self.sqs.meta.events @@ -388,30 +417,32 @@ def test_response_header_capture(self) -> None: # Create a function that sets the custom headers in the after-call event. def modify_after_call_args(parsed, **kwargs): - parsed['ResponseMetadata']['HTTPHeaders'].update(response_headers) + parsed["ResponseMetadata"]["HTTPHeaders"].update(response_headers) # Register the function to an event - event_system.register('after-call.sqs.SendMessage', modify_after_call_args) + event_system.register("after-call.sqs.SendMessage", modify_after_call_args) - queue_url = response['QueueUrl'] + queue_url = response["QueueUrl"] with tracer.start_as_current_span("test"): response = self.sqs.send_message( QueueUrl=queue_url, DelaySeconds=10, MessageAttributes={ - 'Website': { - 'DataType': 'String', - 'StringValue': 'https://www.instana.com' + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", }, }, - MessageBody=('Monitor any application, service, or request ' - 'with Instana Application Performance Monitoring') + MessageBody=( + "Monitor any application, service, or request " + "with Instana Application Performance Monitoring" + ), ) - assert response['MessageId'] + assert response["MessageId"] spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 filter = lambda span: span.n == "sdk" test_span = get_first_span_by_filter(spans, filter) @@ -424,24 +455,35 @@ def modify_after_call_args(parsed, **kwargs): assert boto_span.t == test_span.t assert boto_span.p == test_span.s - assert test_span.ec is None + assert not test_span.ec - assert boto_span.data['boto3']['op'] == 'SendMessage' - assert boto_span.data['boto3']['ep'] == 'https://sqs.us-east-1.amazonaws.com' - assert boto_span.data['boto3']['reg'] == 'us-east-1' + assert boto_span.data["boto3"]["op"] == "SendMessage" + assert boto_span.data["boto3"]["ep"] == "https://sqs.us-east-1.amazonaws.com" + assert boto_span.data["boto3"]["reg"] == "us-east-1" - payload = {'QueueUrl': 'https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME', 'DelaySeconds': 10, - 'MessageAttributes': {'Website': {'DataType': 'String', 'StringValue': 'https://www.instana.com'}}, - 'MessageBody': 'Monitor any application, service, or request with Instana Application Performance Monitoring'} - assert boto_span.data['boto3']['payload'] == payload + payload = { + "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/SQS_QUEUE_NAME", + "DelaySeconds": 10, + "MessageAttributes": { + "Website": { + "DataType": "String", + "StringValue": "https://www.instana.com", + } + }, + "MessageBody": "Monitor any application, service, or request with Instana Application Performance Monitoring", + } + assert boto_span.data["boto3"]["payload"] == payload - assert boto_span.data['http']['status'] == 200 - assert boto_span.data['http']['method'] == 'POST' - assert boto_span.data['http']['url'] == 'https://sqs.us-east-1.amazonaws.com:443/SendMessage' + assert boto_span.data["http"]["status"] == 200 + assert boto_span.data["http"]["method"] == "POST" + assert ( + boto_span.data["http"]["url"] + == "https://sqs.us-east-1.amazonaws.com:443/SendMessage" + ) assert "X-Capture-This-Too" in boto_span.data["http"]["header"] - assert "this too" == boto_span.data["http"]["header"]["X-Capture-This-Too"] + assert boto_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" assert "X-Capture-That-Too" in boto_span.data["http"]["header"] - assert "that too" == boto_span.data["http"]["header"]["X-Capture-That-Too"] + assert boto_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers From db60342acd605e6dc7644027541ba4f536ce5884 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 10 Apr 2024 16:20:36 +0200 Subject: [PATCH 132/172] style: format conftest.py Used ruff (vscode) to: - Black-compatible code formatting. - fix all auto-fixable violations, like unused imports. - isort-compatible import sorting. Signed-off-by: Paulo Vital --- tests/conftest.py | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 790f2ac6..27baed87 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -125,50 +125,3 @@ def celery_enable_logging(): @pytest.fixture(scope="session") def celery_includes(): return {"tests.frameworks.test_celery"} - - -@pytest.fixture -def trace_id() -> int: - return 1812338823475918251 - - -@pytest.fixture -def span_id() -> int: - return 6895521157646639861 - - -@pytest.fixture -def span_processor() -> StanRecorder: - rec = StanRecorder(TestAgent()) - rec.THREAD_NAME = "InstanaSpan Recorder Test" - return rec - - -@pytest.fixture -def tracer_provider(span_processor: StanRecorder) -> InstanaTracerProvider: - return InstanaTracerProvider(span_processor=span_processor, exporter=TestAgent()) - - -@pytest.fixture -def span_context(trace_id: int, span_id: int) -> SpanContext: - return SpanContext( - trace_id=trace_id, - span_id=span_id, - is_remote=False, - ) - - -@pytest.fixture -def span(span_context: SpanContext, span_processor: StanRecorder) -> InstanaSpan: - span_name = "test-span" - return InstanaSpan(span_name, span_context, span_processor) - - -@pytest.fixture -def base_span(span: InstanaSpan) -> BaseSpan: - return BaseSpan(span, None) - - -@pytest.fixture -def context(span: InstanaSpan) -> Context: - return set_span_in_context(span) From 2a10cbb50a07147a8ac2780ee85168cc64456bad Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Sun, 14 Jul 2024 12:04:44 -0700 Subject: [PATCH 133/172] fix(tests): Adapt unit tests after Span structure refactor. Signed-off-by: Paulo Vital --- tests/conftest.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_tracer.py | 9 ++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 27baed87..790f2ac6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -125,3 +125,50 @@ def celery_enable_logging(): @pytest.fixture(scope="session") def celery_includes(): return {"tests.frameworks.test_celery"} + + +@pytest.fixture +def trace_id() -> int: + return 1812338823475918251 + + +@pytest.fixture +def span_id() -> int: + return 6895521157646639861 + + +@pytest.fixture +def span_processor() -> StanRecorder: + rec = StanRecorder(TestAgent()) + rec.THREAD_NAME = "InstanaSpan Recorder Test" + return rec + + +@pytest.fixture +def tracer_provider(span_processor: StanRecorder) -> InstanaTracerProvider: + return InstanaTracerProvider(span_processor=span_processor, exporter=TestAgent()) + + +@pytest.fixture +def span_context(trace_id: int, span_id: int) -> SpanContext: + return SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=False, + ) + + +@pytest.fixture +def span(span_context: SpanContext, span_processor: StanRecorder) -> InstanaSpan: + span_name = "test-span" + return InstanaSpan(span_name, span_context, span_processor) + + +@pytest.fixture +def base_span(span: InstanaSpan) -> BaseSpan: + return BaseSpan(span, None) + + +@pytest.fixture +def context(span: InstanaSpan) -> Context: + return set_span_in_context(span) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 16c2042f..73d69ea4 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -7,9 +7,16 @@ from instana.agent.test import TestAgent from instana.recorder import StanRecorder from instana.sampling import InstanaSampler -from instana.span.span import InstanaSpan, get_current_span, INVALID_SPAN_ID, INVALID_SPAN +from instana.span.span import ( + InstanaSpan, + get_current_span, + INVALID_SPAN_ID, + INVALID_SPAN, +) from instana.span_context import SpanContext from instana.tracer import InstanaTracer, InstanaTracerProvider +from opentelemetry.context.context import Context +from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: From d69f8e7ef669933c71195cbb87ea874560ba8fe6 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 21 Aug 2024 15:14:34 +0200 Subject: [PATCH 134/172] feature: added psycopg2 implementation Signed-off-by: Cagri Yonca --- src/instana/__init__.py | 2 +- src/instana/instrumentation/psycopg2.py | 24 +- src/instana/span/registered_span.py | 16 +- src/instana/util/__init__.py | 36 ++- tests/clients/test_psycopg2.py | 359 +++++++++++++----------- tests/conftest.py | 2 +- 6 files changed, 238 insertions(+), 201 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 2b401a54..45590e92 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -174,7 +174,7 @@ def boot_agent(): logging, # noqa: F401 # mysqlclient, # noqa: F401 # pika, # noqa: F401 - # psycopg2, # noqa: F401 + psycopg2, # noqa: F401 # pymongo, # noqa: F401 # pymysql, # noqa: F401 # redis, # noqa: F401 diff --git a/src/instana/instrumentation/psycopg2.py b/src/instana/instrumentation/psycopg2.py index 86e35b10..43ecf1e3 100644 --- a/src/instana/instrumentation/psycopg2.py +++ b/src/instana/instrumentation/psycopg2.py @@ -5,33 +5,33 @@ import copy import wrapt -from ..log import logger -from .pep0249 import ConnectionFactory +from instana.log import logger +from instana.instrumentation.pep0249 import ConnectionFactory try: import psycopg2 import psycopg2.extras - cf = ConnectionFactory(connect_func=psycopg2.connect, module_name='postgres') + cf = ConnectionFactory(connect_func=psycopg2.connect, module_name="postgres") - setattr(psycopg2, 'connect', cf) - if hasattr(psycopg2, 'Connect'): - setattr(psycopg2, 'Connect', cf) + setattr(psycopg2, "connect", cf) + if hasattr(psycopg2, "Connect"): + setattr(psycopg2, "Connect", cf) - @wrapt.patch_function_wrapper('psycopg2.extensions', 'register_type') + @wrapt.patch_function_wrapper("psycopg2.extensions", "register_type") def register_type_with_instana(wrapped, instance, args, kwargs): args_clone = list(copy.copy(args)) - if (len(args_clone) >= 2) and hasattr(args_clone[1], '__wrapped__'): + if (len(args_clone) >= 2) and hasattr(args_clone[1], "__wrapped__"): args_clone[1] = args_clone[1].__wrapped__ return wrapped(*args_clone, **kwargs) - @wrapt.patch_function_wrapper('psycopg2._json', 'register_json') + @wrapt.patch_function_wrapper("psycopg2._json", "register_json") def register_json_with_instana(wrapped, instance, args, kwargs): - if 'conn_or_curs' in kwargs: - if hasattr(kwargs['conn_or_curs'], '__wrapped__'): - kwargs['conn_or_curs'] = kwargs['conn_or_curs'].__wrapped__ + if "conn_or_curs" in kwargs: + if hasattr(kwargs["conn_or_curs"], "__wrapped__"): + kwargs["conn_or_curs"] = kwargs["conn_or_curs"].__wrapped__ return wrapped(*args, **kwargs) diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py index e8175db0..728d66a8 100644 --- a/src/instana/span/registered_span.py +++ b/src/instana/span/registered_span.py @@ -12,9 +12,7 @@ def __init__(self, span, source, service_name, **kwargs) -> None: # pylint: disable=invalid-name super(RegisteredSpan, self).__init__(span, source, **kwargs) self.n = span.name - self.k = ( - SpanKind.SERVER - ) # entry -> Server span represents a synchronous incoming remote call such as an incoming HTTP request + self.k = SpanKind.SERVER # entry -> Server span represents a synchronous incoming remote call such as an incoming HTTP request self.data["service"] = service_name if span.name in ENTRY_SPANS: @@ -22,14 +20,10 @@ def __init__(self, span, source, service_name, **kwargs) -> None: self._populate_entry_span_data(span) self._populate_extra_span_attributes(span) elif span.name in EXIT_SPANS: - self.k = ( - SpanKind.CLIENT - ) # exit -> Client span represents a synchronous outgoing remote call such as an outgoing HTTP request or database call + self.k = SpanKind.CLIENT # exit -> Client span represents a synchronous outgoing remote call such as an outgoing HTTP request or database call self._populate_exit_span_data(span) elif span.name in LOCAL_SPANS: - self.k = ( - SpanKind.INTERNAL - ) # intermediate -> Internal span represents an internal operation within an application + self.k = SpanKind.INTERNAL # intermediate -> Internal span represents an internal operation within an application self._populate_local_span_data(span) if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": @@ -243,7 +237,7 @@ def _populate_exit_span_data(self, span) -> None: elif span.name == "mysql": self.data["mysql"]["host"] = span.attributes.pop("host", None) self.data["mysql"]["port"] = span.attributes.pop("port", None) - self.data["mysql"]["db"] = span.attributes.pop("db.instance", None) + self.data["mysql"]["db"] = span.attributes.pop("db.name", None) self.data["mysql"]["user"] = span.attributes.pop("db.user", None) self.data["mysql"]["stmt"] = span.attributes.pop("db.statement", None) self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) @@ -251,7 +245,7 @@ def _populate_exit_span_data(self, span) -> None: elif span.name == "postgres": self.data["pg"]["host"] = span.attributes.pop("host", None) self.data["pg"]["port"] = span.attributes.pop("port", None) - self.data["pg"]["db"] = span.attributes.pop("db.instance", None) + self.data["pg"]["db"] = span.attributes.pop("db.name", None) self.data["pg"]["user"] = span.attributes.pop("db.user", None) self.data["pg"]["stmt"] = span.attributes.pop("db.statement", None) self.data["pg"]["error"] = span.attributes.pop("pg.error", None) diff --git a/src/instana/util/__init__.py b/src/instana/util/__init__.py index 97de4f3f..bd991126 100644 --- a/src/instana/util/__init__.py +++ b/src/instana/util/__init__.py @@ -10,9 +10,11 @@ from ..log import logger + def nested_dictionary(): return defaultdict(DictionaryOfStan) + # Simple implementation of a nested dictionary. DictionaryOfStan = nested_dictionary @@ -26,17 +28,21 @@ def to_json(obj): :return: json string """ try: + def extractor(o): - if not hasattr(o, '__dict__'): + if not hasattr(o, "__dict__"): logger.debug("Couldn't serialize non dict type: %s", type(o)) return {} else: return {k.lower(): v for k, v in o.__dict__.items() if v is not None} - return json.dumps(obj, default=extractor, sort_keys=False, separators=(',', ':')).encode() + return json.dumps( + obj, default=extractor, sort_keys=False, separators=(",", ":") + ).encode() except Exception: logger.debug("to_json non-fatal encoding issue: ", exc_info=True) + def to_pretty_json(obj): """ Convert obj to pretty json. Used mostly in logging/debugging. @@ -45,14 +51,17 @@ def to_pretty_json(obj): :return: json string """ try: + def extractor(o): - if not hasattr(o, '__dict__'): + if not hasattr(o, "__dict__"): logger.debug("Couldn't serialize non dict type: %s", type(o)) return {} else: return {k.lower(): v for k, v in o.__dict__.items() if v is not None} - return json.dumps(obj, default=extractor, sort_keys=True, indent=4, separators=(',', ':')) + return json.dumps( + obj, default=extractor, sort_keys=True, indent=4, separators=(",", ":") + ) except Exception: logger.debug("to_pretty_json non-fatal encoding issue: ", exc_info=True) @@ -65,9 +74,9 @@ def package_version(): """ version = "" try: - version = importlib.metadata.version('instana') + version = importlib.metadata.version("instana") except importlib.metadata.PackageNotFoundError: - version = 'unknown' + version = "unknown" return version @@ -85,13 +94,18 @@ def get_default_gateway(): # The Gateway IP is encoded backwards in hex. with open("/proc/self/net/route") as routes: for line in routes: - parts = line.split('\t') - if parts[1] == '00000000': + parts = line.split("\t") + if parts[1] == "00000000": hip = parts[2] if hip is not None and len(hip) == 8: # Reverse order, convert hex to int - return "%i.%i.%i.%i" % (int(hip[6:8], 16), int(hip[4:6], 16), int(hip[2:4], 16), int(hip[0:2], 16)) + return "%i.%i.%i.%i" % ( + int(hip[6:8], 16), + int(hip[4:6], 16), + int(hip[2:4], 16), + int(hip[0:2], 16), + ) except Exception: logger.warning("get_default_gateway: ", exc_info=True) @@ -113,7 +127,9 @@ def every(delay, task, name): if task() is False: break except Exception: - logger.debug("Problem while executing repetitive task: %s", name, exc_info=True) + logger.debug( + "Problem while executing repetitive task: %s", name, exc_info=True + ) # skip tasks if we are behind schedule: next_time += (time.time() - next_time) // delay * delay + delay diff --git a/tests/clients/test_psycopg2.py b/tests/clients/test_psycopg2.py index 7a76d6b8..2fdad702 100644 --- a/tests/clients/test_psycopg2.py +++ b/tests/clients/test_psycopg2.py @@ -2,9 +2,11 @@ # (c) Copyright Instana Inc. 2020 import logging -import unittest +import pytest -from ..helpers import testenv +from typing import Generator +from instana.instrumentation.psycopg2 import register_json_with_instana +from tests.helpers import testenv from instana.singletons import agent, tracer import psycopg2 @@ -14,15 +16,15 @@ logger = logging.getLogger(__name__) -class TestPsycoPG2(unittest.TestCase): - def setUp(self): - deprecated_param_name = self.shortDescription() == 'test_deprecated_parameter_database' +class TestPsycoPG2: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: kwargs = { - 'host': testenv['postgresql_host'], - 'port': testenv['postgresql_port'], - 'user': testenv['postgresql_user'], - 'password': testenv['postgresql_pw'], - 'dbname' if not deprecated_param_name else 'database': testenv['postgresql_db'], + "host": testenv["postgresql_host"], + "port": testenv["postgresql_port"], + "user": testenv["postgresql_user"], + "password": testenv["postgresql_pw"], + "dbname": testenv["postgresql_db"], } self.db = psycopg2.connect(**kwargs) @@ -48,62 +50,64 @@ def setUp(self): cursor = self.db.cursor() cursor.execute(database_setup_query) self.db.commit() - cursor.close() - self.cursor = self.db.cursor() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() tracer.cur_ctx = None - - def tearDown(self): + yield if self.cursor and not self.cursor.connection.closed: - self.cursor.close() + self.cursor.close() if self.db and not self.db.closed: - self.db.close() + self.db.close() agent.options.allow_exit_as_root = False + def test_register_json(self): + resp = register_json_with_instana(conn_or_curs=self.db) + assert resp[0].values[0] == 114 + assert resp[1].values[0] == 199 + def test_vanilla_query(self): - self.assertTrue(psycopg2.extras.register_uuid(None, self.db)) - self.assertTrue(psycopg2.extras.register_uuid(None, self.db.cursor())) + assert psycopg2.extras.register_uuid(None, self.db) + assert psycopg2.extras.register_uuid(None, self.db.cursor()) self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount - self.assertEqual(1, affected_rows) + assert 1 == affected_rows result = self.cursor.fetchone() - self.assertEqual(6, len(result)) + assert 6 == len(result) spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert 0 == len(spans) def test_basic_query(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() self.db.commit() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert 1 == affected_rows + assert 6 == len(result) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert db_span.ec is None - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_basic_query_as_root_exit_span(self): agent.options.allow_exit_as_root = True @@ -112,130 +116,144 @@ def test_basic_query_as_root_exit_span(self): result = self.cursor.fetchone() self.db.commit() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert 1 == affected_rows + assert 6 == len(result) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert 1 == len(spans) db_span = spans[0] - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n, "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_basic_insert(self): - with tracer.start_active_span('test'): - self.cursor.execute("""INSERT INTO users(name, email) VALUES(%s, %s)""", ('beaker', 'beaker@muppets.com')) + with tracer.start_as_current_span("test"): + self.cursor.execute( + """INSERT INTO users(name, email) VALUES(%s, %s)""", + ("beaker", "beaker@muppets.com"), + ) affected_rows = self.cursor.rowcount - self.assertEqual(1, affected_rows) + assert 1 == affected_rows spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert db_span.ec is None - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert ( + db_span.data["pg"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_executemany(self): - with tracer.start_active_span('test'): - self.cursor.executemany("INSERT INTO users(name, email) VALUES(%s, %s)", - [('beaker', 'beaker@muppets.com'), ('beaker', 'beaker@muppets.com')]) + with tracer.start_as_current_span("test"): + self.cursor.executemany( + "INSERT INTO users(name, email) VALUES(%s, %s)", + [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], + ) affected_rows = self.cursor.rowcount self.db.commit() - self.assertEqual(2, affected_rows) + assert 2 == affected_rows spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s + + assert db_span.ec is None - self.assertIsNone(db_span.ec) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert ( + db_span.data["pg"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_call_proc(self): - with tracer.start_active_span('test'): - callproc_result = self.cursor.callproc('test_proc', ('beaker',)) + with tracer.start_as_current_span("test"): + callproc_result = self.cursor.callproc("test_proc", ("beaker",)) - self.assertIsInstance(callproc_result, tuple) + assert isinstance(callproc_result, tuple) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert db_span.ec is None - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'test_proc') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "test_proc" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_error_capture(self): affected_rows = result = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from blah""") affected_rows = self.cursor.rowcount self.cursor.fetchone() except Exception: pass - self.assertIsNone(affected_rows) - self.assertIsNone(result) + assert affected_rows is None + assert result is None spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertEqual(1, db_span.ec) - self.assertEqual(db_span.data["pg"]["error"], 'relation "blah" does not exist\nLINE 1: SELECT * from blah\n ^\n') + assert 2 == db_span.ec + assert db_span.data["pg"]["error"] == ( + 'relation "blah" does not exist\nLINE 1: SELECT * from blah\n ^\n' + ) - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) - self.assertEqual(db_span.data["pg"]["user"], testenv['postgresql_user']) - self.assertEqual(db_span.data["pg"]["stmt"], 'SELECT * from blah') - self.assertEqual(db_span.data["pg"]["host"], testenv['postgresql_host']) - self.assertEqual(db_span.data["pg"]["port"], testenv['postgresql_port']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from blah" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] # Added to validate unicode support and register_type. def test_unicode(self): @@ -245,22 +263,29 @@ def test_unicode(self): self.cursor.execute("delete from users where id in (1,2,3)") # unicode in statement - psycopg2.extras.execute_batch(self.cursor, - "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, [(1, 'x')]) + psycopg2.extras.execute_batch( + self.cursor, + "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, + [(1, "x")], + ) self.cursor.execute("select id, name from users where id = 1") - self.assertEqual(self.cursor.fetchone(), (1, 'x')) + assert self.cursor.fetchone() == (1, "x") # unicode in data - psycopg2.extras.execute_batch(self.cursor, - "insert into users (id, name) values (%s, %s)", [(2, snowman)]) + psycopg2.extras.execute_batch( + self.cursor, "insert into users (id, name) values (%s, %s)", [(2, snowman)] + ) self.cursor.execute("select id, name from users where id = 2") - self.assertEqual(self.cursor.fetchone(), (2, snowman)) + assert self.cursor.fetchone() == (2, snowman) # unicode in both - psycopg2.extras.execute_batch(self.cursor, - "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, [(3, snowman)]) + psycopg2.extras.execute_batch( + self.cursor, + "insert into users (id, name) values (%%s, %%s) -- %s" % snowman, + [(3, snowman)], + ) self.cursor.execute("select id, name from users where id = 3") - self.assertEqual(self.cursor.fetchone(), (3, snowman)) + assert self.cursor.fetchone() == (3, snowman) def test_register_type(self): import uuid @@ -268,121 +293,123 @@ def test_register_type(self): oid1 = 2950 oid2 = 2951 - ext.UUID = ext.new_type((oid1,), "UUID", lambda data, cursor: data and uuid.UUID(data) or None) + ext.UUID = ext.new_type( + (oid1,), "UUID", lambda data, cursor: data and uuid.UUID(data) or None + ) ext.UUIDARRAY = ext.new_array_type((oid2,), "UUID[]", ext.UUID) ext.register_type(ext.UUID, self.cursor) ext.register_type(ext.UUIDARRAY, self.cursor) def test_connect_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") affected_rows = cursor.rowcount result = cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert 1 == affected_rows + assert 6 == len(result) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert db_span.ec is None - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv["postgresql_db"]) - self.assertEqual(db_span.data["pg"]["user"], testenv["postgresql_user"]) - self.assertEqual(db_span.data["pg"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["pg"]["host"], testenv["postgresql_host"]) - self.assertEqual(db_span.data["pg"]["port"], testenv["postgresql_port"]) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_connect_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") affected_rows = cursor.rowcount result = cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert 1 == affected_rows + assert 6 == len(result) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert db_span.ec is None - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv["postgresql_db"]) - self.assertEqual(db_span.data["pg"]["user"], testenv["postgresql_user"]) - self.assertEqual(db_span.data["pg"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["pg"]["host"], testenv["postgresql_host"]) - self.assertEqual(db_span.data["pg"]["port"], testenv["postgresql_port"]) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") affected_rows = cursor.rowcount result = cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert 1 == affected_rows + assert 6 == len(result) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert db_span.ec is None - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv["postgresql_db"]) - self.assertEqual(db_span.data["pg"]["user"], testenv["postgresql_user"]) - self.assertEqual(db_span.data["pg"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["pg"]["host"], testenv["postgresql_host"]) - self.assertEqual(db_span.data["pg"]["port"], testenv["postgresql_port"]) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] + assert db_span.data["pg"]["user"] == testenv["postgresql_user"] + assert db_span.data["pg"]["stmt"] == "SELECT * from users" + assert db_span.data["pg"]["host"] == testenv["postgresql_host"] + assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_deprecated_parameter_database(self): """test_deprecated_parameter_database""" - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() self.db.commit() - self.assertEqual(1, affected_rows) - self.assertEqual(6, len(result)) + assert 1 == affected_rows + assert 6 == len(result) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert 2 == len(spans) db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert "test" == test_span.data["sdk"]["name"] + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert db_span.ec is None - self.assertEqual(db_span.n, "postgres") - self.assertEqual(db_span.data["pg"]["db"], testenv['postgresql_db']) + assert db_span.n == "postgres" + assert db_span.data["pg"]["db"] == testenv["postgresql_db"] diff --git a/tests/conftest.py b/tests/conftest.py index 790f2ac6..9044b356 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,12 +37,12 @@ # TODO: remove the following entries as the migration of the instrumentation # codes are finalised. +collect_ignore_glob.append("*clients/boto*") collect_ignore_glob.append("*clients/test_cassandra*") collect_ignore_glob.append("*clients/test_couchbase*") collect_ignore_glob.append("*clients/test_google*") collect_ignore_glob.append("*clients/test_mysql*") collect_ignore_glob.append("*clients/test_pika*") -collect_ignore_glob.append("*clients/test_psycopg*") collect_ignore_glob.append("*clients/test_pym*") collect_ignore_glob.append("*clients/test_redis*") collect_ignore_glob.append("*clients/test_sql*") From 8637bdeff0eea042c5df93a9194055d5b34d1483 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Mon, 26 Aug 2024 21:14:16 +0200 Subject: [PATCH 135/172] update: added typing annotations Signed-off-by: Cagri Yonca --- src/instana/instrumentation/psycopg2.py | 15 +++++++++++-- tests/clients/test_psycopg2.py | 28 ++++++++++++------------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/instana/instrumentation/psycopg2.py b/src/instana/instrumentation/psycopg2.py index 43ecf1e3..97cc594f 100644 --- a/src/instana/instrumentation/psycopg2.py +++ b/src/instana/instrumentation/psycopg2.py @@ -5,6 +5,7 @@ import copy import wrapt +from typing import Callable, Optional, Any, Tuple, Dict from instana.log import logger from instana.instrumentation.pep0249 import ConnectionFactory @@ -19,7 +20,12 @@ setattr(psycopg2, "Connect", cf) @wrapt.patch_function_wrapper("psycopg2.extensions", "register_type") - def register_type_with_instana(wrapped, instance, args, kwargs): + def register_type_with_instana( + wrapped: Callable[..., Any], + instance: Optional[Any], + args: Tuple[Any, ...], + kwargs: Dict[str, Any], + ) -> Callable[..., Any]: args_clone = list(copy.copy(args)) if (len(args_clone) >= 2) and hasattr(args_clone[1], "__wrapped__"): @@ -28,7 +34,12 @@ def register_type_with_instana(wrapped, instance, args, kwargs): return wrapped(*args_clone, **kwargs) @wrapt.patch_function_wrapper("psycopg2._json", "register_json") - def register_json_with_instana(wrapped, instance, args, kwargs): + def register_json_with_instana( + wrapped: Callable[..., Any], + instance: Optional[Any], + args: Tuple[Any, ...], + kwargs: Dict[str, Any], + ) -> Callable[..., Any]: if "conn_or_curs" in kwargs: if hasattr(kwargs["conn_or_curs"], "__wrapped__"): kwargs["conn_or_curs"] = kwargs["conn_or_curs"].__wrapped__ diff --git a/tests/clients/test_psycopg2.py b/tests/clients/test_psycopg2.py index 2fdad702..dc0142a1 100644 --- a/tests/clients/test_psycopg2.py +++ b/tests/clients/test_psycopg2.py @@ -62,12 +62,12 @@ def _resource(self) -> Generator[None, None, None]: self.db.close() agent.options.allow_exit_as_root = False - def test_register_json(self): + def test_register_json(self) -> None: resp = register_json_with_instana(conn_or_curs=self.db) assert resp[0].values[0] == 114 assert resp[1].values[0] == 199 - def test_vanilla_query(self): + def test_vanilla_query(self) -> None: assert psycopg2.extras.register_uuid(None, self.db) assert psycopg2.extras.register_uuid(None, self.db.cursor()) @@ -81,7 +81,7 @@ def test_vanilla_query(self): spans = self.recorder.queued_spans() assert 0 == len(spans) - def test_basic_query(self): + def test_basic_query(self) -> None: with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount @@ -109,7 +109,7 @@ def test_basic_query(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_basic_query_as_root_exit_span(self): + def test_basic_query_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount @@ -133,7 +133,7 @@ def test_basic_query_as_root_exit_span(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_basic_insert(self): + def test_basic_insert(self) -> None: with tracer.start_as_current_span("test"): self.cursor.execute( """INSERT INTO users(name, email) VALUES(%s, %s)""", @@ -164,7 +164,7 @@ def test_basic_insert(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_executemany(self): + def test_executemany(self) -> None: with tracer.start_as_current_span("test"): self.cursor.executemany( "INSERT INTO users(name, email) VALUES(%s, %s)", @@ -197,7 +197,7 @@ def test_executemany(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_call_proc(self): + def test_call_proc(self) -> None: with tracer.start_as_current_span("test"): callproc_result = self.cursor.callproc("test_proc", ("beaker",)) @@ -221,7 +221,7 @@ def test_call_proc(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_error_capture(self): + def test_error_capture(self) -> None: affected_rows = result = None try: with tracer.start_as_current_span("test"): @@ -256,7 +256,7 @@ def test_error_capture(self): assert db_span.data["pg"]["port"] == testenv["postgresql_port"] # Added to validate unicode support and register_type. - def test_unicode(self): + def test_unicode(self) -> None: ext.register_type(ext.UNICODE, self.cursor) snowman = "\u2603" @@ -287,7 +287,7 @@ def test_unicode(self): self.cursor.execute("select id, name from users where id = 3") assert self.cursor.fetchone() == (3, snowman) - def test_register_type(self): + def test_register_type(self) -> None: import uuid oid1 = 2950 @@ -301,7 +301,7 @@ def test_register_type(self): ext.register_type(ext.UUID, self.cursor) ext.register_type(ext.UUIDARRAY, self.cursor) - def test_connect_cursor_ctx_mgr(self): + def test_connect_cursor_ctx_mgr(self) -> None: with tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: @@ -330,7 +330,7 @@ def test_connect_cursor_ctx_mgr(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_connect_ctx_mgr(self): + def test_connect_ctx_mgr(self) -> None: with tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() @@ -359,7 +359,7 @@ def test_connect_ctx_mgr(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_cursor_ctx_mgr(self): + def test_cursor_ctx_mgr(self) -> None: with tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: @@ -388,7 +388,7 @@ def test_cursor_ctx_mgr(self): assert db_span.data["pg"]["host"] == testenv["postgresql_host"] assert db_span.data["pg"]["port"] == testenv["postgresql_port"] - def test_deprecated_parameter_database(self): + def test_deprecated_parameter_database(self) -> None: """test_deprecated_parameter_database""" with tracer.start_as_current_span("test"): From 6e4a051b4720c44ad30f1eec638277756dbbffdb Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 28 Aug 2024 11:16:06 +0200 Subject: [PATCH 136/172] fix: fixed assertions Signed-off-by: Cagri Yonca --- src/instana/instrumentation/psycopg2.py | 4 +- tests/clients/test_psycopg2.py | 96 ++++++++++++------------- 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/src/instana/instrumentation/psycopg2.py b/src/instana/instrumentation/psycopg2.py index 97cc594f..6045c4c9 100644 --- a/src/instana/instrumentation/psycopg2.py +++ b/src/instana/instrumentation/psycopg2.py @@ -25,7 +25,7 @@ def register_type_with_instana( instance: Optional[Any], args: Tuple[Any, ...], kwargs: Dict[str, Any], - ) -> Callable[..., Any]: + ) -> Callable[..., object]: args_clone = list(copy.copy(args)) if (len(args_clone) >= 2) and hasattr(args_clone[1], "__wrapped__"): @@ -39,7 +39,7 @@ def register_json_with_instana( instance: Optional[Any], args: Tuple[Any, ...], kwargs: Dict[str, Any], - ) -> Callable[..., Any]: + ) -> Callable[..., object]: if "conn_or_curs" in kwargs: if hasattr(kwargs["conn_or_curs"], "__wrapped__"): kwargs["conn_or_curs"] = kwargs["conn_or_curs"].__wrapped__ diff --git a/tests/clients/test_psycopg2.py b/tests/clients/test_psycopg2.py index dc0142a1..17b88bb4 100644 --- a/tests/clients/test_psycopg2.py +++ b/tests/clients/test_psycopg2.py @@ -73,13 +73,13 @@ def test_vanilla_query(self) -> None: self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount - assert 1 == affected_rows + assert affected_rows == 1 result = self.cursor.fetchone() - assert 6 == len(result) + assert len(result) == 6 spans = self.recorder.queued_spans() - assert 0 == len(spans) + assert len(spans) == 0 def test_basic_query(self) -> None: with tracer.start_as_current_span("test"): @@ -88,19 +88,19 @@ def test_basic_query(self) -> None: result = self.cursor.fetchone() self.db.commit() - assert 1 == affected_rows - assert 6 == len(result) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] @@ -116,11 +116,11 @@ def test_basic_query_as_root_exit_span(self) -> None: result = self.cursor.fetchone() self.db.commit() - assert 1 == affected_rows - assert 6 == len(result) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - assert 1 == len(spans) + assert len(spans) == 1 db_span = spans[0] @@ -141,18 +141,18 @@ def test_basic_insert(self) -> None: ) affected_rows = self.cursor.rowcount - assert 1 == affected_rows + assert affected_rows == 1 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] @@ -173,18 +173,18 @@ def test_executemany(self) -> None: affected_rows = self.cursor.rowcount self.db.commit() - assert 2 == affected_rows + assert affected_rows == 2 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] @@ -204,15 +204,15 @@ def test_call_proc(self) -> None: assert isinstance(callproc_result, tuple) spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] @@ -231,19 +231,19 @@ def test_error_capture(self) -> None: except Exception: pass - assert affected_rows is None - assert result is None + assert not affected_rows + assert not result spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert 2 == db_span.ec + assert db_span.ec == 2 assert db_span.data["pg"]["error"] == ( 'relation "blah" does not exist\nLINE 1: SELECT * from blah\n ^\n' ) @@ -309,19 +309,19 @@ def test_connect_cursor_ctx_mgr(self) -> None: affected_rows = cursor.rowcount result = cursor.fetchone() - assert 1 == affected_rows - assert 6 == len(result) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] @@ -338,19 +338,19 @@ def test_connect_ctx_mgr(self) -> None: affected_rows = cursor.rowcount result = cursor.fetchone() - assert 1 == affected_rows - assert 6 == len(result) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] @@ -367,19 +367,19 @@ def test_cursor_ctx_mgr(self) -> None: affected_rows = cursor.rowcount result = cursor.fetchone() - assert 1 == affected_rows - assert 6 == len(result) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] @@ -389,27 +389,25 @@ def test_cursor_ctx_mgr(self) -> None: assert db_span.data["pg"]["port"] == testenv["postgresql_port"] def test_deprecated_parameter_database(self) -> None: - """test_deprecated_parameter_database""" - with tracer.start_as_current_span("test"): self.cursor.execute("""SELECT * from users""") affected_rows = self.cursor.rowcount result = self.cursor.fetchone() self.db.commit() - assert 1 == affected_rows - assert 6 == len(result) + assert affected_rows == 1 + assert len(result) == 6 spans = self.recorder.queued_spans() - assert 2 == len(spans) + assert len(spans) == 2 db_span, test_span = spans - assert "test" == test_span.data["sdk"]["name"] + assert test_span.data["sdk"]["name"] == "test" assert test_span.t == db_span.t assert db_span.p == test_span.s - assert db_span.ec is None + assert not db_span.ec assert db_span.n == "postgres" assert db_span.data["pg"]["db"] == testenv["postgresql_db"] From f4eae644d9ce63bef085aa44f417b3dcbe7bb6f3 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 28 Aug 2024 11:56:16 +0200 Subject: [PATCH 137/172] imported pep0249 to instrumentation Signed-off-by: Cagri Yonca --- src/instana/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 45590e92..0f6988d1 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -174,6 +174,7 @@ def boot_agent(): logging, # noqa: F401 # mysqlclient, # noqa: F401 # pika, # noqa: F401 + pep0249, # noqa: F401 psycopg2, # noqa: F401 # pymongo, # noqa: F401 # pymysql, # noqa: F401 @@ -183,6 +184,7 @@ def boot_agent(): # sanic_inst, # noqa: F401 urllib3, # noqa: F401 ) + # from instana.instrumentation.aiohttp import ( # client, # noqa: F401 # server, # noqa: F401 From b7fe9044b333f314b83986dcce5bfec750289fec Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Fri, 30 Aug 2024 09:53:33 +0200 Subject: [PATCH 138/172] update: updated typing annotations Signed-off-by: Cagri Yonca --- tests/clients/test_pep0249.py | 37 +++++++++++++++++------------------ 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/clients/test_pep0249.py b/tests/clients/test_pep0249.py index 0bef6d81..6235e6cc 100644 --- a/tests/clients/test_pep0249.py +++ b/tests/clients/test_pep0249.py @@ -12,7 +12,6 @@ ) from instana.singletons import tracer from instana.span.span import InstanaSpan -from instana.util.traceutils import get_tracer_tuple from opentelemetry.trace import SpanKind from pytest import LogCaptureFixture @@ -52,7 +51,7 @@ def _resource(self) -> Generator[None, None, None]: self.test_cursor.close() self.test_conn.close() - def reset_table(self): + def reset_table(self) -> None: self.test_cursor.execute( """ DROP TABLE IF EXISTS tests; @@ -66,7 +65,7 @@ def reset_table(self): ) self.test_conn.commit() - def reset_procedure(self): + def reset_procedure(self) -> None: self.test_cursor.execute(""" DROP PROCEDURE IF EXISTS insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR); CREATE PROCEDURE insert_user(IN test_id INT, IN test_name VARCHAR, IN test_email VARCHAR) @@ -79,7 +78,7 @@ def reset_procedure(self): """) self.test_conn.commit() - def test_cursor_wrapper_default(self): + def test_cursor_wrapper_default(self) -> None: # CursorWrapper assert self.test_wrapper assert self.test_wrapper._module_name == self.cursor_name @@ -110,7 +109,7 @@ def test_cursor_wrapper_default(self): assert hasattr(self.test_cursor, "fetchone") assert hasattr(self.test_cursor, "fetchall") - def test_collect_kvs(self): + def test_collect_kvs(self) -> None: self.reset_table() with tracer.start_as_current_span("test") as span: sample_sql = """ @@ -124,7 +123,7 @@ def test_collect_kvs(self): assert span.attributes["host"] == "127.0.0.1" assert span.attributes["port"] == 5432 - def test_collect_kvs_error(self, caplog: LogCaptureFixture): + def test_collect_kvs_error(self, caplog: LogCaptureFixture) -> None: self.reset_table() with tracer.start_as_current_span("test") as span: connect_params = "sample" @@ -138,12 +137,12 @@ def test_collect_kvs_error(self, caplog: LogCaptureFixture): sample_wrapper._collect_kvs(span, sample_sql) assert "string indices must be integers" in caplog.messages[0] - def test_enter(self): + def test_enter(self) -> None: response = self.test_wrapper.__enter__() assert response == self.test_wrapper assert isinstance(response, CursorWrapper) - def test_execute_with_tracing_off(self): + def test_execute_with_tracing_off(self) -> None: self.reset_table() with tracer.start_as_current_span("sqlalchemy"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" @@ -154,7 +153,7 @@ def test_execute_with_tracing_off(self): assert sample_params in response assert len(response) == 2 - def test_execute_with_tracing(self): + def test_execute_with_tracing(self) -> None: self.reset_table() with tracer.start_as_current_span("test"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" @@ -176,7 +175,7 @@ def test_execute_with_tracing(self): assert sample_params in response assert len(response) == 2 - def test_executemany_with_tracing_off(self): + def test_executemany_with_tracing_off(self) -> None: self.reset_table() with tracer.start_as_current_span("sqlalchemy"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" @@ -191,7 +190,7 @@ def test_executemany_with_tracing_off(self): assert record in response assert len(response) == 3 - def test_executemany_with_tracing(self): + def test_executemany_with_tracing(self) -> None: self.reset_table() with tracer.start_as_current_span("test"): sample_sql = """insert into tests (id, name, email) values (%s, %s, %s) returning id, name, email;""" @@ -216,7 +215,7 @@ def test_executemany_with_tracing(self): assert record in response assert len(response) == 3 - def test_callproc_with_tracing_off(self): + def test_callproc_with_tracing_off(self) -> None: self.reset_table() self.reset_procedure() with tracer.start_as_current_span("sqlalchemy"): @@ -229,7 +228,7 @@ def test_callproc_with_tracing_off(self): assert sample_params in response assert len(response) == 2 - def test_callproc_with_tracing(self): + def test_callproc_with_tracing(self) -> None: self.reset_table() self.reset_procedure() with tracer.start_as_current_span("test"): @@ -280,26 +279,26 @@ def _resource(self) -> Generator[None, None, None]: yield self.test_conn.close() - def test_enter(self): + def test_enter(self) -> None: response = self.connection_manager.__enter__() assert isinstance(response, ConnectionWrapper) assert response._module_name == self.module_name assert response._connect_params == self.connect_params - def test_cursor(self): + def test_cursor(self) -> None: response = self.connection_manager.cursor() assert isinstance(response, CursorWrapper) - def test_close(self): + def test_close(self) -> None: response = self.connection_manager.close() assert self.test_conn.closed assert not response - def test_commit(self): + def test_commit(self) -> None: response = self.connection_manager.commit() assert not response - def test_rollback(self): + def test_rollback(self) -> None: if hasattr(self.connection_manager, "rollback"): response = self.connection_manager.rollback() assert not response @@ -316,7 +315,7 @@ def _resource(self) -> Generator[None, None, None]: self.test_module_name = None self.conn_fact = None - def test_call(self): + def test_call(self) -> None: response = self.conn_fact( dsn="user=root password=passw0rd dbname=instana_test_db host=localhost port=5432" ) From 6762c76123056c79900a34dc4405a168cc59d47f Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 2 Sep 2024 14:31:21 +0200 Subject: [PATCH 139/172] fix: Adapt logging tests to OTel after rebase. Signed-off-by: Paulo Vital --- tests/clients/test_logging.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 9c2b223b..9d107651 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -120,8 +120,9 @@ def test_log_caller(self): def log_custom_warning(): self.logger.warning("foo %s", "bar") - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): log_custom_warning() - self.assertEqual(self.caplog.records[0].funcName, "log_custom_warning") + + assert self.caplog.records[-1].funcName == "log_custom_warning" self.logger.removeHandler(handler) From f77197f06626d8cbd9afe314e99ac51b745377eb Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Mon, 2 Sep 2024 15:25:53 +0200 Subject: [PATCH 140/172] tests(logging): Refactor to pure pytest UT. Signed-off-by: Paulo Vital --- tests/clients/test_logging.py | 42 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/clients/test_logging.py b/tests/clients/test_logging.py index 9d107651..6ec666c5 100644 --- a/tests/clients/test_logging.py +++ b/tests/clients/test_logging.py @@ -2,28 +2,27 @@ # (c) Copyright Instana Inc. 2020 import logging -import unittest +from typing import Generator from unittest.mock import patch +import pytest from opentelemetry.trace import SpanKind -import pytest from instana.singletons import agent, tracer -class TestLogging(unittest.TestCase): - - @pytest.fixture - def capture_log(self, caplog): - self.caplog = caplog - def setUp(self) -> None: - """Clear all spans before a test run""" +class TestLogging: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run self.recorder = tracer.span_processor self.recorder.clear_spans() self.logger = logging.getLogger("unit test") - - def tearDown(self) -> None: - """Ensure that allow_exit_as_root has the default value""" + yield + # tearDown + # Ensure that allow_exit_as_root has the default value agent.options.allow_exit_as_root = False def test_no_span(self) -> None: @@ -32,6 +31,7 @@ def test_no_span(self) -> None: self.logger.info("info message") spans = self.recorder.queued_spans() + assert len(spans) == 1 def test_extra_span(self) -> None: @@ -39,9 +39,9 @@ def test_extra_span(self) -> None: self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() + assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["log"].get("message") == "foo bar" def test_log_with_tuple(self) -> None: @@ -49,9 +49,9 @@ def test_log_with_tuple(self) -> None: self.logger.warning("foo %s", ("bar",)) spans = self.recorder.queued_spans() + assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["log"].get("message") == "foo ('bar',)" def test_log_with_dict(self) -> None: @@ -59,9 +59,9 @@ def test_log_with_dict(self) -> None: self.logger.warning("foo %s", {"bar": 18}) spans = self.recorder.queued_spans() + assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["log"].get("message") == "foo {'bar': 18}" def test_parameters(self) -> None: @@ -74,8 +74,8 @@ def test_parameters(self) -> None: self.logger.exception("Exception: %s", str(e)) spans = self.recorder.queued_spans() - assert len(spans) == 2 + assert len(spans) == 2 assert spans[0].data["log"].get("parameters") is not None def test_no_root_exit_span(self) -> None: @@ -83,6 +83,7 @@ def test_no_root_exit_span(self) -> None: self.logger.info("info message") spans = self.recorder.queued_spans() + assert len(spans) == 0 def test_root_exit_span(self) -> None: @@ -90,9 +91,9 @@ def test_root_exit_span(self) -> None: self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() + assert len(spans) == 1 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["log"].get("message") == "foo bar" def test_exception(self) -> None: @@ -104,13 +105,12 @@ def test_exception(self) -> None: self.logger.warning("foo %s", "bar") spans = self.recorder.queued_spans() + assert len(spans) == 2 assert spans[0].k is SpanKind.CLIENT - assert spans[0].data["log"] == {} - @pytest.mark.usefixtures("capture_log") - def test_log_caller(self): + def test_log_caller(self, caplog: pytest.LogCaptureFixture) -> None: handler = logging.StreamHandler() handler.setFormatter( logging.Formatter("source: %(funcName)s, message: %(message)s") @@ -123,6 +123,6 @@ def log_custom_warning(): with tracer.start_as_current_span("test"): log_custom_warning() - assert self.caplog.records[-1].funcName == "log_custom_warning" + assert caplog.records[-1].funcName == "log_custom_warning" self.logger.removeHandler(handler) From c6e742f64853571bef36e43d2a78bdf2a0027bf4 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 4 Sep 2024 10:07:08 +0300 Subject: [PATCH 141/172] fix: refactor of unittests Signed-off-by: Cagri Yonca --- tests/conftest.py | 1 - tests/test_tracer_provider.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9044b356..31bd4950 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,6 @@ # TODO: remove the following entries as the migration of the instrumentation # codes are finalised. -collect_ignore_glob.append("*clients/boto*") collect_ignore_glob.append("*clients/test_cassandra*") collect_ignore_glob.append("*clients/test_couchbase*") collect_ignore_glob.append("*clients/test_google*") diff --git a/tests/test_tracer_provider.py b/tests/test_tracer_provider.py index b4b9f8bc..f2933485 100644 --- a/tests/test_tracer_provider.py +++ b/tests/test_tracer_provider.py @@ -37,7 +37,7 @@ def test_tracer_provider_get_tracer_empty_instrumenting_module_name( provider = InstanaTracerProvider() tracer = provider.get_tracer("") - assert "get_tracer called with missing module name." == caplog.record_tuples[0][2] + assert "get_tracer called with missing module name." in caplog.messages assert isinstance(tracer, InstanaTracer) From 8f9d89686e98733d1efa0226a416e156233533a5 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 3 Sep 2024 12:40:53 +0300 Subject: [PATCH 142/172] refactor(pymysql): added instrumentation of pymysql Signed-off-by: Cagri Yonca --- src/instana/__init__.py | 2 +- src/instana/instrumentation/pymysql.py | 14 +++++++------- src/instana/span/registered_span.py | 7 ++++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 0f6988d1..1654f01f 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -177,7 +177,7 @@ def boot_agent(): pep0249, # noqa: F401 psycopg2, # noqa: F401 # pymongo, # noqa: F401 - # pymysql, # noqa: F401 + pymysql, # noqa: F401 # redis, # noqa: F401 # sqlalchemy, # noqa: F401 starlette_inst, # noqa: F401 diff --git a/src/instana/instrumentation/pymysql.py b/src/instana/instrumentation/pymysql.py index c4939cc4..50cf9b3d 100644 --- a/src/instana/instrumentation/pymysql.py +++ b/src/instana/instrumentation/pymysql.py @@ -2,17 +2,17 @@ # (c) Copyright Instana Inc. 2019 -from ..log import logger -from .pep0249 import ConnectionFactory +from instana.log import logger +from instana.instrumentation.pep0249 import ConnectionFactory try: - import pymysql # + import pymysql - cf = ConnectionFactory(connect_func=pymysql.connect, module_name='mysql') + cf = ConnectionFactory(connect_func=pymysql.connect, module_name="mysql") - setattr(pymysql, 'connect', cf) - if hasattr(pymysql, 'Connect'): - setattr(pymysql, 'Connect', cf) + setattr(pymysql, "connect", cf) + if hasattr(pymysql, "Connect"): + setattr(pymysql, "Connect", cf) logger.debug("Instrumenting pymysql") except ImportError: diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py index 728d66a8..afb38a97 100644 --- a/src/instana/span/registered_span.py +++ b/src/instana/span/registered_span.py @@ -5,6 +5,7 @@ from instana.span.kind import ENTRY_SPANS, EXIT_SPANS, HTTP_SPANS, LOCAL_SPANS from opentelemetry.trace import SpanKind +from opentelemetry.semconv.trace import SpanAttributes class RegisteredSpan(BaseSpan): @@ -237,9 +238,9 @@ def _populate_exit_span_data(self, span) -> None: elif span.name == "mysql": self.data["mysql"]["host"] = span.attributes.pop("host", None) self.data["mysql"]["port"] = span.attributes.pop("port", None) - self.data["mysql"]["db"] = span.attributes.pop("db.name", None) - self.data["mysql"]["user"] = span.attributes.pop("db.user", None) - self.data["mysql"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["mysql"]["db"] = span.attributes.pop(SpanAttributes.DB_NAME, None) + self.data["mysql"]["user"] = span.attributes.pop(SpanAttributes.DB_USER, None) + self.data["mysql"]["stmt"] = span.attributes.pop(SpanAttributes.DB_STATEMENT, None) self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) elif span.name == "postgres": From 30fa801160f997d6071a016398fee4a0ec880e91 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 3 Sep 2024 12:41:18 +0300 Subject: [PATCH 143/172] unittests(pymysql): added refactor of unittests Signed-off-by: Cagri Yonca --- tests/clients/test_pymysql.py | 380 +++++++++++++++++----------------- tests/conftest.py | 2 +- 2 files changed, 196 insertions(+), 186 deletions(-) diff --git a/tests/clients/test_pymysql.py b/tests/clients/test_pymysql.py index 4479b698..8e4793d5 100644 --- a/tests/clients/test_pymysql.py +++ b/tests/clients/test_pymysql.py @@ -1,26 +1,25 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import logging -import unittest +import time +import pytest import pymysql -from ..helpers import testenv +from typing import Generator +from tests.helpers import testenv from instana.singletons import agent, tracer -logger = logging.getLogger(__name__) - -class TestPyMySQL(unittest.TestCase): - def setUp(self): - deprecated_param_name = self.shortDescription() == 'test_deprecated_parameter_db' +class TestPyMySQL: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: kwargs = { - 'host': testenv['mysql_host'], - 'port': testenv['mysql_port'], - 'user': testenv['mysql_user'], - 'passwd': testenv['mysql_pw'], - 'database' if not deprecated_param_name else 'db': testenv['mysql_db'], + "host": testenv["mysql_host"], + "port": testenv["mysql_port"], + "user": testenv["mysql_user"], + "passwd": testenv["mysql_pw"], + "database": testenv["mysql_db"], } self.db = pymysql.connect(**kwargs) @@ -39,303 +38,314 @@ def setUp(self): END """ setup_cursor = self.db.cursor() - for s in database_setup_query.split('|'): - setup_cursor.execute(s) + for s in database_setup_query.split("|"): + setup_cursor.execute(s) self.cursor = self.db.cursor() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() tracer.cur_ctx = None - - def tearDown(self): + yield if self.cursor and self.cursor.connection.open: - self.cursor.close() + self.cursor.close() if self.db and self.db.open: - self.db.close() + self.db.close() agent.options.allow_exit_as_root = False - def test_vanilla_query(self): + def test_vanilla_query(self) -> None: affected_rows = self.cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 result = self.cursor.fetchone() - self.assertEqual(3, len(result)) + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 - def test_basic_query(self): - with tracer.start_active_span('test'): + def test_basic_query(self) -> None: + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_basic_query_as_root_exit_span(self): + def test_basic_query_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 db_span = spans[0] - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_query_with_params(self): - with tracer.start_active_span('test'): + def test_query_with_params(self) -> None: + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users where id=1""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users where id=?') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users where id=?" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_basic_insert(self): - with tracer.start_active_span('test'): + def test_basic_insert(self) -> None: + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute( - """INSERT INTO users(name, email) VALUES(%s, %s)""", - ('beaker', 'beaker@muppets.com')) + """INSERT INTO users(name, email) VALUES(%s, %s)""", + ("beaker", "beaker@muppets.com"), + ) - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) - - self.assertIsNone(db_span.ec) - - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) - - def test_executemany(self): - with tracer.start_active_span('test'): - affected_rows = self.cursor.executemany("INSERT INTO users(name, email) VALUES(%s, %s)", - [('beaker', 'beaker@muppets.com'), ('beaker', 'beaker@muppets.com')]) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s + + assert not db_span.ec + + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] + + def test_executemany(self) -> None: + with tracer.start_as_current_span("test"): + affected_rows = self.cursor.executemany( + "INSERT INTO users(name, email) VALUES(%s, %s)", + [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], + ) self.db.commit() - self.assertEqual(2, affected_rows) + assert affected_rows == 2 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_call_proc(self): - with tracer.start_active_span('test'): - callproc_result = self.cursor.callproc('test_proc', ('beaker',)) + def test_call_proc(self) -> None: + with tracer.start_as_current_span("test"): + callproc_result = self.cursor.callproc("test_proc", ("beaker",)) - self.assertIsInstance(callproc_result, tuple) + assert isinstance(callproc_result, tuple) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'test_proc') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "test_proc" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_error_capture(self): + def test_error_capture(self) -> None: affected_rows = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from blah""") except Exception: pass - self.assertIsNone(affected_rows) + assert not affected_rows spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) - self.assertEqual(1, db_span.ec) - - self.assertEqual(db_span.data["mysql"]["error"], u'(1146, "Table \'%s.blah\' doesn\'t exist")' % testenv['mysql_db']) - - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from blah') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) - - def test_connect_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s + assert db_span.ec == 2 + + assert ( + db_span.data["mysql"]["error"] + == f"(1146, \"Table '{testenv['mysql_db']}.blah' doesn't exist\")" + ) + + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from blah" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] + + def test_connect_cursor_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_connect_ctx_mgr(self): - with tracer.start_active_span("test"): + def test_connect_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + def test_cursor_ctx_mgr(self) -> None: + with tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: cursor.execute("""SELECT * from users""") - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] - def test_deprecated_parameter_db(self): + def test_deprecated_parameter_db(self) -> None: """test_deprecated_parameter_db""" - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] diff --git a/tests/conftest.py b/tests/conftest.py index 31bd4950..1e180620 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,7 +42,7 @@ collect_ignore_glob.append("*clients/test_google*") collect_ignore_glob.append("*clients/test_mysql*") collect_ignore_glob.append("*clients/test_pika*") -collect_ignore_glob.append("*clients/test_pym*") +collect_ignore_glob.append("*clients/test_pymongo*") collect_ignore_glob.append("*clients/test_redis*") collect_ignore_glob.append("*clients/test_sql*") From 89b548a5139b8221978e130166d1b02ab30b0546 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 3 Sep 2024 12:59:06 +0300 Subject: [PATCH 144/172] refactor(mysqlclient): added instrumentation of mysqlclient Signed-off-by: Cagri Yonca --- src/instana/__init__.py | 2 +- src/instana/instrumentation/mysqlclient.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 1654f01f..f7db3304 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -172,7 +172,7 @@ def boot_agent(): # gevent_inst, # noqa: F401 # grpcio, # noqa: F401 logging, # noqa: F401 - # mysqlclient, # noqa: F401 + mysqlclient, # noqa: F401 # pika, # noqa: F401 pep0249, # noqa: F401 psycopg2, # noqa: F401 diff --git a/src/instana/instrumentation/mysqlclient.py b/src/instana/instrumentation/mysqlclient.py index 5b7270f8..82165869 100644 --- a/src/instana/instrumentation/mysqlclient.py +++ b/src/instana/instrumentation/mysqlclient.py @@ -2,17 +2,17 @@ # (c) Copyright Instana Inc. 2019 -from ..log import logger -from .pep0249 import ConnectionFactory +from instana.log import logger +from instana.instrumentation.pep0249 import ConnectionFactory try: import MySQLdb - cf = ConnectionFactory(connect_func=MySQLdb.connect, module_name='mysql') + cf = ConnectionFactory(connect_func=MySQLdb.connect, module_name="mysql") - setattr(MySQLdb, 'connect', cf) - if hasattr(MySQLdb, 'Connect'): - setattr(MySQLdb, 'Connect', cf) + setattr(MySQLdb, "connect", cf) + if hasattr(MySQLdb, "Connect"): + setattr(MySQLdb, "Connect", cf) logger.debug("Instrumenting mysqlclient") except ImportError: From ec2635918c1b68bee287bb19ae837fc284795e09 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 3 Sep 2024 13:00:01 +0300 Subject: [PATCH 145/172] unittest(mysqlclient): added unittests and typing annotations Signed-off-by: Cagri Yonca --- tests/clients/test_mysqlclient.py | 300 +++++++++++++++--------------- tests/conftest.py | 1 - 2 files changed, 155 insertions(+), 146 deletions(-) diff --git a/tests/clients/test_mysqlclient.py b/tests/clients/test_mysqlclient.py index 518eff30..4f5f6013 100644 --- a/tests/clients/test_mysqlclient.py +++ b/tests/clients/test_mysqlclient.py @@ -1,22 +1,23 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import logging -import unittest - import MySQLdb - -from ..helpers import testenv -from instana.singletons import agent, tracer - -logger = logging.getLogger(__name__) - - -class TestMySQLPython(unittest.TestCase): - def setUp(self): - self.db = MySQLdb.connect(host=testenv['mysql_host'], port=testenv['mysql_port'], - user=testenv['mysql_user'], passwd=testenv['mysql_pw'], - db=testenv['mysql_db']) +import pytest + +from instana.singletons import agent, tracer +from tests.helpers import testenv + + +class TestMySQLPython: + @pytest.fixture(autouse=True) + def _resource(self): + self.db = MySQLdb.connect( + host=testenv["mysql_host"], + port=testenv["mysql_port"], + user=testenv["mysql_user"], + passwd=testenv["mysql_pw"], + db=testenv["mysql_db"], + ) database_setup_query = """ DROP TABLE IF EXISTS users; CREATE TABLE users( @@ -36,251 +37,260 @@ def setUp(self): setup_cursor.close() self.cursor = self.db.cursor() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() tracer.cur_ctx = None - - def tearDown(self): + yield if self.cursor and self.cursor.connection.open: - self.cursor.close() + self.cursor.close() if self.db and self.db.open: - self.db.close() + self.db.close() agent.options.allow_exit_as_root = False def test_vanilla_query(self): affected_rows = self.cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 result = self.cursor.fetchone() - self.assertEqual(3, len(result)) + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 def test_basic_query(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_basic_query_as_root_exit_span(self): agent.options.allow_exit_as_root = True affected_rows = self.cursor.execute("""SELECT * from users""") result = self.cursor.fetchone() - self.assertEqual(1, affected_rows) - self.assertEqual(3, len(result)) + assert affected_rows == 1 + assert len(result) == 3 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 db_span = spans[0] - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from users') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_basic_insert(self): - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute( - """INSERT INTO users(name, email) VALUES(%s, %s)""", - ('beaker', 'beaker@muppets.com')) + """INSERT INTO users(name, email) VALUES(%s, %s)""", + ("beaker", "beaker@muppets.com"), + ) - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_executemany(self): - with tracer.start_active_span('test'): - affected_rows = self.cursor.executemany("INSERT INTO users(name, email) VALUES(%s, %s)", - [('beaker', 'beaker@muppets.com'), ('beaker', 'beaker@muppets.com')]) + with tracer.start_as_current_span("test"): + affected_rows = self.cursor.executemany( + "INSERT INTO users(name, email) VALUES(%s, %s)", + [("beaker", "beaker@muppets.com"), ("beaker", "beaker@muppets.com")], + ) self.db.commit() - self.assertEqual(2, affected_rows) + assert affected_rows == 2 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'INSERT INTO users(name, email) VALUES(%s, %s)') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert ( + db_span.data["mysql"]["stmt"] + == "INSERT INTO users(name, email) VALUES(%s, %s)" + ) + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_call_proc(self): - with tracer.start_active_span('test'): - callproc_result = self.cursor.callproc('test_proc', ('beaker',)) + with tracer.start_as_current_span("test"): + callproc_result = self.cursor.callproc("test_proc", ("beaker",)) - self.assertIsInstance(callproc_result, tuple) + assert isinstance(callproc_result, tuple) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'test_proc') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "test_proc" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_error_capture(self): affected_rows = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): affected_rows = self.cursor.execute("""SELECT * from blah""") except Exception: pass - self.assertIsNone(affected_rows) + assert not affected_rows spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertEqual(1, db_span.ec) - self.assertEqual(db_span.data["mysql"]["error"], '(1146, "Table \'%s.blah\' doesn\'t exist")' % testenv['mysql_db']) + assert db_span.ec == 2 + assert ( + db_span.data["mysql"]["error"] + == f"(1146, \"Table '{testenv['mysql_db']}.blah' doesn't exist\")" + ) - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv['mysql_db']) - self.assertEqual(db_span.data["mysql"]["user"], testenv['mysql_user']) - self.assertEqual(db_span.data["mysql"]["stmt"], 'SELECT * from blah') - self.assertEqual(db_span.data["mysql"]["host"], testenv['mysql_host']) - self.assertEqual(db_span.data["mysql"]["port"], testenv['mysql_port']) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from blah" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): with self.db as connection: with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_connect_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): with self.db as connection: cursor = connection.cursor() cursor.execute("""SELECT * from users""") - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] def test_cursor_ctx_mgr(self): - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): connection = self.db with connection.cursor() as cursor: affected_rows = cursor.execute("""SELECT * from users""") - - self.assertEqual(1, affected_rows) + assert affected_rows == 1 spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 db_span, test_span = spans - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.data["sdk"]["name"] == "test" + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mysql") - self.assertEqual(db_span.data["mysql"]["db"], testenv["mysql_db"]) - self.assertEqual(db_span.data["mysql"]["user"], testenv["mysql_user"]) - self.assertEqual(db_span.data["mysql"]["stmt"], "SELECT * from users") - self.assertEqual(db_span.data["mysql"]["host"], testenv["mysql_host"]) - self.assertEqual(db_span.data["mysql"]["port"], testenv["mysql_port"]) + assert db_span.n == "mysql" + assert db_span.data["mysql"]["db"] == testenv["mysql_db"] + assert db_span.data["mysql"]["user"] == testenv["mysql_user"] + assert db_span.data["mysql"]["stmt"] == "SELECT * from users" + assert db_span.data["mysql"]["host"] == testenv["mysql_host"] + assert db_span.data["mysql"]["port"] == testenv["mysql_port"] diff --git a/tests/conftest.py b/tests/conftest.py index 1e180620..e2161a6c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,7 +40,6 @@ collect_ignore_glob.append("*clients/test_cassandra*") collect_ignore_glob.append("*clients/test_couchbase*") collect_ignore_glob.append("*clients/test_google*") -collect_ignore_glob.append("*clients/test_mysql*") collect_ignore_glob.append("*clients/test_pika*") collect_ignore_glob.append("*clients/test_pymongo*") collect_ignore_glob.append("*clients/test_redis*") From 2e8a375002fced8007d31b44b5033a5208573917 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 3 Sep 2024 14:41:24 +0300 Subject: [PATCH 146/172] refactor(pymongo): added otel instrumentation of pymongo Signed-off-by: Cagri Yonca --- src/instana/__init__.py | 2 +- src/instana/instrumentation/pymongo.py | 65 +++++++++++++++----------- src/instana/span/registered_span.py | 10 +--- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index f7db3304..2173a3d4 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -176,7 +176,7 @@ def boot_agent(): # pika, # noqa: F401 pep0249, # noqa: F401 psycopg2, # noqa: F401 - # pymongo, # noqa: F401 + pymongo, # noqa: F401 pymysql, # noqa: F401 # redis, # noqa: F401 # sqlalchemy, # noqa: F401 diff --git a/src/instana/instrumentation/pymongo.py b/src/instana/instrumentation/pymongo.py index 264fd658..2c0bc203 100644 --- a/src/instana/instrumentation/pymongo.py +++ b/src/instana/instrumentation/pymongo.py @@ -2,43 +2,49 @@ # (c) Copyright Instana Inc. 2020 -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.span.span import InstanaSpan +from instana.log import logger +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import pymongo - from pymongo import monitoring from bson import json_util + from opentelemetry.semconv.trace import SpanAttributes - - class MongoCommandTracer(monitoring.CommandListener): - def __init__(self): + class MongoCommandTracer(pymongo.monitoring.CommandListener): + def __init__(self) -> None: self.__active_commands = {} - def started(self, event): + def started(self, event: pymongo.monitoring.CommandStartedEvent) -> None: tracer, parent_span, _ = get_tracer_tuple() # return early if we're not tracing if tracing_is_off(): return + parent_context = parent_span.get_span_context() if parent_span else None - with tracer.start_active_span("mongo", child_of=parent_span) as scope: - self._collect_connection_tags(scope.span, event) - self._collect_command_tags(scope.span, event) + with tracer.start_as_current_span( + "mongo", span_context=parent_context + ) as span: + self._collect_connection_tags(span, event) + self._collect_command_tags(span, event) # include collection name into the namespace if provided if event.command_name in event.command: - scope.span.set_tag("collection", event.command.get(event.command_name)) + span.set_attribute( + SpanAttributes.DB_MONGODB_COLLECTION, + event.command.get(event.command_name), + ) - self.__active_commands[event.request_id] = scope + self.__active_commands[event.request_id] = span - def succeeded(self, event): + def succeeded(self, event: pymongo.monitoring.CommandStartedEvent) -> None: active_span = self.__active_commands.pop(event.request_id, None) # return early if we're not tracing if active_span is None: return - def failed(self, event): + def failed(self, event: pymongo.monitoring.CommandStartedEvent) -> None: active_span = self.__active_commands.pop(event.request_id, None) # return early if we're not tracing @@ -47,23 +53,27 @@ def failed(self, event): active_span.log_exception(event.failure) - def _collect_connection_tags(self, span, event): + def _collect_connection_tags( + self, span: InstanaSpan, event: pymongo.monitoring.CommandStartedEvent + ) -> None: (host, port) = event.connection_id - span.set_tag("host", host) - span.set_tag("port", str(port)) - span.set_tag("db", event.database_name) + span.set_attribute(SpanAttributes.SERVER_ADDRESS, host) + span.set_attribute(SpanAttributes.SERVER_PORT, str(port)) + span.set_attribute(SpanAttributes.DB_NAME, event.database_name) - def _collect_command_tags(self, span, event): + def _collect_command_tags(self, span, event) -> None: """ Extract MongoDB command name and arguments and attach it to the span """ cmd = event.command_name - span.set_tag("command", cmd) + span.set_attribute("command", cmd) for key in ["filter", "query"]: if key in event.command: - span.set_tag("filter", json_util.dumps(event.command.get(key))) + span.set_attribute( + "filter", json_util.dumps(event.command.get(key)) + ) break # The location of command documents within the command object depends on the name @@ -72,24 +82,25 @@ def _collect_command_tags(self, span, event): "insert": "documents", "update": "updates", "delete": "deletes", - "aggregate": "pipeline" + "aggregate": "pipeline", } cmd_doc = None if cmd in cmd_doc_locations: cmd_doc = event.command.get(cmd_doc_locations[cmd]) - elif cmd.lower() == "mapreduce": # mapreduce command was renamed to mapReduce in pymongo 3.9.0 + elif ( + cmd.lower() == "mapreduce" + ): # mapreduce command was renamed to mapReduce in pymongo 3.9.0 # mapreduce command consists of two mandatory parts: map and reduce cmd_doc = { "map": event.command.get("map"), - "reduce": event.command.get("reduce") + "reduce": event.command.get("reduce"), } if cmd_doc is not None: - span.set_tag("json", json_util.dumps(cmd_doc)) - + span.set_attribute("json", json_util.dumps(cmd_doc)) - monitoring.register(MongoCommandTracer()) + pymongo.monitoring.register(MongoCommandTracer()) logger.debug("Instrumenting pymongo") diff --git a/src/instana/span/registered_span.py b/src/instana/span/registered_span.py index afb38a97..6164ca86 100644 --- a/src/instana/span/registered_span.py +++ b/src/instana/span/registered_span.py @@ -252,14 +252,8 @@ def _populate_exit_span_data(self, span) -> None: self.data["pg"]["error"] = span.attributes.pop("pg.error", None) elif span.name == "mongo": - service = "%s:%s" % ( - span.attributes.pop("host", None), - span.attributes.pop("port", None), - ) - namespace = "%s.%s" % ( - span.attributes.pop("db", "?"), - span.attributes.pop("collection", "?"), - ) + service = f"{span.attributes.pop(SpanAttributes.SERVER_ADDRESS, None)}:{span.attributes.pop(SpanAttributes.SERVER_PORT, None)}" + namespace = f"{span.attributes.pop(SpanAttributes.DB_NAME, '?')}.{span.attributes.pop(SpanAttributes.DB_MONGODB_COLLECTION, '?')}" self.data["mongo"]["service"] = service self.data["mongo"]["namespace"] = namespace From a0c9ecbda359c99bdaca719b9f116f14c7b5baa7 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Tue, 3 Sep 2024 15:03:06 +0300 Subject: [PATCH 147/172] unittests(pymongo): added unittests for instrumentation Signed-off-by: Cagri Yonca --- tests/clients/test_pymongo.py | 319 +++++++++++++++++++--------------- tests/conftest.py | 1 - tests/test_tracer_provider.py | 4 +- 3 files changed, 178 insertions(+), 146 deletions(-) diff --git a/tests/clients/test_pymongo.py b/tests/clients/test_pymongo.py index b54b0525..251f0b40 100644 --- a/tests/clients/test_pymongo.py +++ b/tests/clients/test_pymongo.py @@ -2,256 +2,289 @@ # (c) Copyright Instana Inc. 2020 import json -import unittest import logging +from typing import Generator -from ..helpers import testenv -from instana.singletons import agent, tracer - -import pymongo import bson +import pymongo +import pytest -logger = logging.getLogger(__name__) +from instana.singletons import agent, tracer +from instana.span.span import get_current_span +from tests.helpers import testenv -pymongoversion = unittest.skipIf( - pymongo.version_tuple >= (4, 0), reason="map reduce is removed in pymongo 4.0" -) +logger = logging.getLogger(__name__) -class TestPyMongoTracer(unittest.TestCase): - def setUp(self): - self.client = pymongo.MongoClient(host=testenv['mongodb_host'], port=int(testenv['mongodb_port']), - username=testenv['mongodb_user'], password=testenv['mongodb_pw']) +class TestPyMongoTracer: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.client = pymongo.MongoClient( + host=testenv["mongodb_host"], + port=int(testenv["mongodb_port"]), + username=testenv["mongodb_user"], + password=testenv["mongodb_pw"], + ) self.client.test.records.delete_many(filter={}) - - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() - - def tearDown(self): + yield self.client.close() agent.options.allow_exit_as_root = False - def test_successful_find_query(self): - with tracer.start_active_span("test"): + def test_successful_find_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.find_one({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "find") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "find" - self.assertEqual(db_span.data["mongo"]["filter"], '{"type": "string"}') - self.assertIsNone(db_span.data["mongo"]["json"]) + assert db_span.data["mongo"]["filter"] == '{"type": "string"}' + assert not db_span.data["mongo"]["json"] - def test_successful_find_query_as_root_span(self): + def test_successful_find_query_as_root_span(self) -> None: agent.options.allow_exit_as_root = True self.client.test.records.find_one({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 1) + assert len(spans) == 1 db_span = spans[0] - self.assertEqual(db_span.p, None) - - self.assertIsNone(db_span.ec) + assert not db_span.p + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "find") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "find" - self.assertEqual(db_span.data["mongo"]["filter"], '{"type": "string"}') - self.assertIsNone(db_span.data["mongo"]["json"]) + assert db_span.data["mongo"]["filter"] == '{"type": "string"}' + assert not db_span.data["mongo"]["json"] - def test_successful_insert_query(self): - with tracer.start_active_span("test"): + def test_successful_insert_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.insert_one({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "insert") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "insert" - self.assertIsNone(db_span.data["mongo"]["filter"]) + assert not db_span.data["mongo"]["filter"] - def test_successful_update_query(self): - with tracer.start_active_span("test"): - self.client.test.records.update_one({"type": "string"}, {"$set": {"type": "int"}}) - - self.assertIsNone(tracer.active_span) + def test_successful_update_query(self) -> None: + with tracer.start_as_current_span("test"): + self.client.test.records.update_one( + {"type": "string"}, {"$set": {"type": "int"}} + ) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "update") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "update" - self.assertIsNone(db_span.data["mongo"]["filter"]) - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert not db_span.data["mongo"]["filter"] + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertIn({ - "q": {"type": "string"}, - "u": {"$set": {"type": "int"}}, - "multi": False, - "upsert": False - }, payload) - - def test_successful_delete_query(self): - with tracer.start_active_span("test"): + assert { + "q": {"type": "string"}, + "u": {"$set": {"type": "int"}}, + "multi": False, + "upsert": False, + } in payload + + def test_successful_delete_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.delete_one(filter={"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "delete") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "delete" - self.assertIsNone(db_span.data["mongo"]["filter"]) - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert not db_span.data["mongo"]["filter"] + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertIn({"q": {"type": "string"}, "limit": 1}, payload) + assert {"q": {"type": "string"}, "limit": 1} in payload - def test_successful_aggregate_query(self): - with tracer.start_active_span("test"): + def test_successful_aggregate_query(self) -> None: + with tracer.start_as_current_span("test"): self.client.test.records.count_documents({"type": "string"}) - - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"], "aggregate") + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert db_span.data["mongo"]["command"] == "aggregate" - self.assertIsNone(db_span.data["mongo"]["filter"]) - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert not db_span.data["mongo"]["filter"] + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertIn({"$match": {"type": "string"}}, payload) + assert {"$match": {"type": "string"}} in payload - @pymongoversion - def test_successful_map_reduce_query(self): + @pytest.mark.skipif( + pymongo.version_tuple >= (4, 0), reason="map reduce is removed in pymongo 4.0" + ) + def test_successful_map_reduce_query(self) -> None: mapper = "function () { this.tags.forEach(function(z) { emit(z, 1); }); }" reducer = "function (key, values) { return len(values); }" - with tracer.start_active_span("test"): - self.client.test.records.map_reduce(bson.code.Code(mapper), bson.code.Code(reducer), "results", - query={"x": {"$lt": 2}}) - - self.assertIsNone(tracer.active_span) + with tracer.start_as_current_span("test"): + self.client.test.records.map_reduce( + bson.code.Code(mapper), + bson.code.Code(reducer), + "results", + query={"x": {"$lt": 2}}, + ) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 2) + assert len(spans) == 2 db_span = spans[0] test_span = spans[1] - self.assertEqual(test_span.t, db_span.t) - self.assertEqual(db_span.p, test_span.s) + assert test_span.t == db_span.t + assert db_span.p == test_span.s - self.assertIsNone(db_span.ec) + assert not db_span.ec - self.assertEqual(db_span.n, "mongo") - self.assertEqual(db_span.data["mongo"]["service"], "%s:%s" % (testenv['mongodb_host'], testenv['mongodb_port'])) - self.assertEqual(db_span.data["mongo"]["namespace"], "test.records") - self.assertEqual(db_span.data["mongo"]["command"].lower(), - "mapreduce") # mapreduce command was renamed to mapReduce in pymongo 3.9.0 + assert db_span.n == "mongo" + assert ( + db_span.data["mongo"]["service"] + == f"{testenv['mongodb_host']}:{testenv['mongodb_port']}" + ) + assert db_span.data["mongo"]["namespace"] == "test.records" + assert ( + db_span.data["mongo"]["command"].lower() == "mapreduce" + ) # mapreduce command was renamed to mapReduce in pymongo 3.9.0 - self.assertEqual(db_span.data["mongo"]["filter"], '{"x": {"$lt": 2}}') - self.assertIsNotNone(db_span.data["mongo"]["json"]) + assert db_span.data["mongo"]["filter"] == '{"x": {"$lt": 2}}' + assert db_span.data["mongo"]["json"] payload = json.loads(db_span.data["mongo"]["json"]) - self.assertEqual(payload["map"], {"$code": mapper}, db_span.data["mongo"]["json"]) - self.assertEqual(payload["reduce"], {"$code": reducer}, db_span.data["mongo"]["json"]) - - def test_successful_mutiple_queries(self): - with tracer.start_active_span("test"): - self.client.test.records.bulk_write([pymongo.InsertOne({"type": "string"}), - pymongo.UpdateOne({"type": "string"}, {"$set": {"type": "int"}}), - pymongo.DeleteOne({"type": "string"})]) - - self.assertIsNone(tracer.active_span) + assert payload["map"], {"$code": mapper} == db_span.data["mongo"]["json"] + assert payload["reduce"], {"$code": reducer} == db_span.data["mongo"]["json"] + + def test_successful_mutiple_queries(self) -> None: + with tracer.start_as_current_span("test"): + self.client.test.records.bulk_write( + [ + pymongo.InsertOne({"type": "string"}), + pymongo.UpdateOne({"type": "string"}, {"$set": {"type": "int"}}), + pymongo.DeleteOne({"type": "string"}), + ] + ) + current_span = get_current_span() + assert not current_span.is_recording() spans = self.recorder.queued_spans() - self.assertEqual(len(spans), 4) + assert len(spans) == 4 test_span = spans.pop() seen_span_ids = set() commands = [] for span in spans: - self.assertEqual(test_span.t, span.t) - self.assertEqual(span.p, test_span.s) + assert test_span.t == span.t + assert span.p == test_span.s # check if all spans got a unique id - self.assertNotIn(span.s, seen_span_ids) + assert span.s not in seen_span_ids seen_span_ids.add(span.s) commands.append(span.data["mongo"]["command"]) # ensure spans are ordered the same way as commands - self.assertListEqual(commands, ["insert", "update", "delete"]) - + assert commands == ["insert", "update", "delete"] diff --git a/tests/conftest.py b/tests/conftest.py index e2161a6c..e5e49643 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,7 +41,6 @@ collect_ignore_glob.append("*clients/test_couchbase*") collect_ignore_glob.append("*clients/test_google*") collect_ignore_glob.append("*clients/test_pika*") -collect_ignore_glob.append("*clients/test_pymongo*") collect_ignore_glob.append("*clients/test_redis*") collect_ignore_glob.append("*clients/test_sql*") diff --git a/tests/test_tracer_provider.py b/tests/test_tracer_provider.py index f2933485..2a55203b 100644 --- a/tests/test_tracer_provider.py +++ b/tests/test_tracer_provider.py @@ -1,5 +1,7 @@ # (c) Copyright IBM Corp. 2024 +from pytest import LogCaptureFixture + from instana.agent.host import HostAgent from instana.agent.test import TestAgent from instana.propagators.binary_propagator import BinaryPropagator @@ -9,8 +11,6 @@ from instana.recorder import StanRecorder from instana.sampling import InstanaSampler from instana.tracer import InstanaTracer, InstanaTracerProvider -from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID -from pytest import LogCaptureFixture def test_tracer_provider_defaults() -> None: From 885cb1079eb374c82ec2605babd8fafb23c6dd20 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 28 Aug 2024 11:23:42 +0200 Subject: [PATCH 148/172] refactor: Asyncio instrumentation Signed-off-by: Paulo Vital --- src/instana/__init__.py | 2 +- src/instana/configurator.py | 9 +-- src/instana/instrumentation/asyncio.py | 91 +++++++++++++++++++------- src/instana/span/kind.py | 2 +- src/instana/util/traceutils.py | 9 +-- 5 files changed, 77 insertions(+), 36 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 2173a3d4..e511599d 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -163,7 +163,7 @@ def boot_agent(): # Import & initialize instrumentation from instana.instrumentation import ( - # asyncio, # noqa: F401 + asyncio, # noqa: F401 boto3_inst, # noqa: F401 # cassandra_inst, # noqa: F401 # couchbase_inst, # noqa: F401 diff --git a/src/instana/configurator.py b/src/instana/configurator.py index 167aa4c1..65efb35d 100644 --- a/src/instana/configurator.py +++ b/src/instana/configurator.py @@ -5,7 +5,8 @@ This file contains a config object that will hold configuration options for the package. Defaults are set and can be overridden after package load. """ -from .util import DictionaryOfStan + +from instana.util import DictionaryOfStan # La Protagonista config = DictionaryOfStan() @@ -13,8 +14,4 @@ # This option determines if tasks created via asyncio (with ensure_future or create_task) will # automatically carry existing context into the created task. -config['asyncio_task_context_propagation']['enabled'] = False - - - - +config["asyncio_task_context_propagation"]["enabled"] = False diff --git a/src/instana/instrumentation/asyncio.py b/src/instana/instrumentation/asyncio.py index 146f7c90..070dfe85 100644 --- a/src/instana/instrumentation/asyncio.py +++ b/src/instana/instrumentation/asyncio.py @@ -2,46 +2,89 @@ # (c) Copyright Instana Inc. 2019 +import time +from contextlib import contextmanager +from typing import Any, Callable, Dict, Iterator, Tuple + import wrapt -from opentracing.scope_managers.constants import ACTIVE_ATTR -from opentracing.scope_managers.contextvars import no_parent_scope +from opentelemetry.trace import use_span +from opentelemetry.trace.status import StatusCode -from ..configurator import config -from ..log import logger -from ..singletons import async_tracer +from instana.configurator import config +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import asyncio @wrapt.patch_function_wrapper("asyncio", "ensure_future") - def ensure_future_with_instana(wrapped, instance, argv, kwargs): - if config["asyncio_task_context_propagation"]["enabled"] is False: - with no_parent_scope(): - return wrapped(*argv, **kwargs) - - scope = async_tracer.scope_manager.active - task = wrapped(*argv, **kwargs) - - if scope is not None: - setattr(task, ACTIVE_ATTR, scope) + def ensure_future_with_instana( + wrapped: Callable[..., asyncio.ensure_future], + instance: object, + argv: Tuple[object, Tuple[object, ...]], + kwargs: Dict[str, Any], + ) -> object: + if ( + not config["asyncio_task_context_propagation"]["enabled"] + or tracing_is_off() + ): + return wrapped(*argv, **kwargs) - return task + with _start_as_current_async_span() as span: + try: + span.set_status(StatusCode.OK) + return wrapped(*argv, **kwargs) + except Exception as exc: + logger.debug(f"asyncio ensure_future_with_instana error: {exc}") if hasattr(asyncio, "create_task"): @wrapt.patch_function_wrapper("asyncio", "create_task") - def create_task_with_instana(wrapped, instance, argv, kwargs): - if config["asyncio_task_context_propagation"]["enabled"] is False: - with no_parent_scope(): + def create_task_with_instana( + wrapped: Callable[..., asyncio.create_task], + instance: object, + argv: Tuple[object, Tuple[object, ...]], + kwargs: Dict[str, Any], + ) -> object: + if ( + not config["asyncio_task_context_propagation"]["enabled"] + or tracing_is_off() + ): + return wrapped(*argv, **kwargs) + + with _start_as_current_async_span() as span: + try: + span.set_status(StatusCode.OK) return wrapped(*argv, **kwargs) + except Exception as exc: + logger.debug(f"asyncio create_task_with_instana error: {exc}") - scope = async_tracer.scope_manager.active - task = wrapped(*argv, **kwargs) + @contextmanager + def _start_as_current_async_span() -> Iterator[InstanaSpan]: + """ + Creates and yield a special InstanaSpan to only propagate the Asyncio + context. + """ + tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None - if scope is not None: - setattr(task, ACTIVE_ATTR, scope) + _time = time.time_ns() - return task + span = InstanaSpan( + name="asyncio", + context=parent_context, + span_processor=tracer.span_processor, + start_time=_time, + end_time=_time, + ) + with use_span( + span, + end_on_exit=False, + record_exception=False, + set_status_on_exception=False, + ) as span: + yield span logger.debug("Instrumenting asyncio") except ImportError: diff --git a/src/instana/span/kind.py b/src/instana/span/kind.py index 263018fb..9fd7b340 100644 --- a/src/instana/span/kind.py +++ b/src/instana/span/kind.py @@ -6,7 +6,7 @@ EXIT_KIND = ("exit", "client", "producer", SpanKind.CLIENT, SpanKind.PRODUCER) -LOCAL_SPANS = ("render", SpanKind.INTERNAL) +LOCAL_SPANS = ("asyncio", "render", SpanKind.INTERNAL) HTTP_SPANS = ( "aiohttp-client", diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index f7a35af3..c21b058b 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -22,12 +22,13 @@ def extract_custom_headers(tracing_span, headers) -> None: def get_active_tracer() -> Optional[InstanaTracer]: try: - # ToDo: Might have to add additional stuff when testing with async and tornado tracer current_span = get_current_span() - if current_span and current_span.is_recording(): - return tracer - else: + if current_span: + # asyncio Spans are used as NonRecording Spans solely for context propagation + if current_span.is_recording() or current_span.name == "asyncio": + return tracer return None + return None except Exception: # Do not try to log this with instana, as there is no active tracer and there will be an infinite loop at least # for PY2 From 7df0d8645aaf92e11d8eac3cc9cb580bc2addd60 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Fri, 23 Aug 2024 14:27:12 +0200 Subject: [PATCH 149/172] refactor: AIOHTTP server and client instrumentation Signed-off-by: Paulo Vital --- src/instana/__init__.py | 9 +- src/instana/instrumentation/aiohttp/client.py | 116 +++++++++++------- src/instana/instrumentation/aiohttp/server.py | 89 ++++++++------ src/instana/propagators/http_propagator.py | 4 +- 4 files changed, 131 insertions(+), 87 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index e511599d..7ecf67a1 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -184,11 +184,10 @@ def boot_agent(): # sanic_inst, # noqa: F401 urllib3, # noqa: F401 ) - - # from instana.instrumentation.aiohttp import ( - # client, # noqa: F401 - # server, # noqa: F401 - # ) + from instana.instrumentation.aiohttp import ( + client, # noqa: F401 + server, # noqa: F401 + ) # from instana.instrumentation.aws import lambda_inst # noqa: F401 # from instana.instrumentation.celery import hooks # noqa: F401 from instana.instrumentation.django import middleware # noqa: F401 diff --git a/src/instana/instrumentation/aiohttp/client.py b/src/instana/instrumentation/aiohttp/client.py index 3b5b4eb1..4b307dc4 100644 --- a/src/instana/instrumentation/aiohttp/client.py +++ b/src/instana/instrumentation/aiohttp/client.py @@ -2,85 +2,111 @@ # (c) Copyright Instana Inc. 2019 -import opentracing +from types import SimpleNamespace +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple import wrapt -from ...log import logger -from ...singletons import agent, async_tracer -from ...util.secrets import strip_secrets_from_query -from ...util.traceutils import tracing_is_off +from opentelemetry.semconv.trace import SpanAttributes + +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent +from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import aiohttp - import asyncio + if TYPE_CHECKING: + from aiohttp.client import ClientSession + from instana.span.span import InstanaSpan - async def stan_request_start(session, trace_config_ctx, params): + async def stan_request_start( + session: "ClientSession", trace_config_ctx: SimpleNamespace, params + ) -> Awaitable[None]: try: # If we're not tracing, just return if tracing_is_off(): - trace_config_ctx.scope = None + trace_config_ctx.span_context = None return - scope = async_tracer.start_active_span("aiohttp-client", child_of=async_tracer.active_span) - trace_config_ctx.scope = scope + tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None + + span = tracer.start_span("aiohttp-client", span_context=parent_context) - async_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, params.headers) + tracer.inject(span.context, Format.HTTP_HEADERS, params.headers) - parts = str(params.url).split('?') + parts = str(params.url).split("?") if len(parts) > 1: - cleaned_qp = strip_secrets_from_query(parts[1], agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", cleaned_qp) - scope.span.set_tag("http.url", parts[0]) - scope.span.set_tag('http.method', params.method) + cleaned_qp = strip_secrets_from_query( + parts[1], agent.options.secrets_matcher, agent.options.secrets_list + ) + span.set_attribute("http.params", cleaned_qp) + span.set_attribute(SpanAttributes.HTTP_URL, parts[0]) + span.set_attribute(SpanAttributes.HTTP_METHOD, params.method) + trace_config_ctx.span_context = span except Exception: - logger.debug("stan_request_start", exc_info=True) + logger.debug("aiohttp-client stan_request_start error:", exc_info=True) - - async def stan_request_end(session, trace_config_ctx, params): + async def stan_request_end( + session: "ClientSession", trace_config_ctx: SimpleNamespace, params + ) -> Awaitable[None]: try: - scope = trace_config_ctx.scope - if scope is not None: - scope.span.set_tag('http.status_code', params.response.status) + span: "InstanaSpan" = trace_config_ctx.span_context + if span: + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, params.response.status + ) - if agent.options.extra_http_headers is not None: + if agent.options.extra_http_headers: for custom_header in agent.options.extra_http_headers: if custom_header in params.response.headers: - scope.span.set_tag("http.header.%s" % custom_header, params.response.headers[custom_header]) + span.set_attribute( + "http.header.%s" % custom_header, + params.response.headers[custom_header], + ) if 500 <= params.response.status: - scope.span.mark_as_errored({"http.error": params.response.reason}) + span.mark_as_errored({"http.error": params.response.reason}) - scope.close() + if span.is_recording(): + span.end() + trace_config_ctx = None except Exception: - logger.debug("stan_request_end", exc_info=True) - + logger.debug("aiohttp-client stan_request_end error:", exc_info=True) - async def stan_request_exception(session, trace_config_ctx, params): + async def stan_request_exception( + session: "ClientSession", trace_config_ctx: SimpleNamespace, params + ) -> Awaitable[None]: try: - scope = trace_config_ctx.scope - if scope is not None: - scope.span.log_exception(params.exception) - scope.span.set_tag("http.error", str(params.exception)) - scope.close() + span: "InstanaSpan" = trace_config_ctx.span_context + if span: + span.record_exception(params.exception) + span.set_attribute("http.error", str(params.exception)) + if span.is_recording(): + span.end() + trace_config_ctx = None except Exception: - logger.debug("stan_request_exception", exc_info=True) - - - @wrapt.patch_function_wrapper('aiohttp.client', 'ClientSession.__init__') - def init_with_instana(wrapped, instance, argv, kwargs): + logger.debug("aiohttp-client stan_request_exception error:", exc_info=True) + + @wrapt.patch_function_wrapper("aiohttp.client", "ClientSession.__init__") + def init_with_instana( + wrapped: Callable[..., Awaitable["ClientSession"]], + instance: aiohttp.client.ClientSession, + args: Tuple[int, str, Tuple[object, ...]], + kwargs: Dict[str, Any], + ) -> object: instana_trace_config = aiohttp.TraceConfig() instana_trace_config.on_request_start.append(stan_request_start) instana_trace_config.on_request_end.append(stan_request_end) instana_trace_config.on_request_exception.append(stan_request_exception) - if 'trace_configs' in kwargs: - kwargs['trace_configs'].append(instana_trace_config) + if "trace_configs" in kwargs: + kwargs["trace_configs"].append(instana_trace_config) else: - kwargs['trace_configs'] = [instana_trace_config] - - return wrapped(*argv, **kwargs) + kwargs["trace_configs"] = [instana_trace_config] + return wrapped(*args, **kwargs) logger.debug("Instrumenting aiohttp client") except ImportError: diff --git a/src/instana/instrumentation/aiohttp/server.py b/src/instana/instrumentation/aiohttp/server.py index 93dcb256..3036e81d 100644 --- a/src/instana/instrumentation/aiohttp/server.py +++ b/src/instana/instrumentation/aiohttp/server.py @@ -2,44 +2,58 @@ # (c) Copyright Instana Inc. 2019 -import opentracing +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Tuple + import wrapt +from opentelemetry.semconv.trace import SpanAttributes + +from instana.log import logger +from instana.propagators.format import Format +from instana.singletons import agent, tracer +from instana.util.secrets import strip_secrets_from_query -from ...log import logger -from ...singletons import agent, async_tracer -from ...util.secrets import strip_secrets_from_query +if TYPE_CHECKING: + from instana.span.span import InstanaSpan try: import aiohttp - import asyncio - from aiohttp.web import middleware + if TYPE_CHECKING: + import aiohttp.web @middleware - async def stan_middleware(request, handler): + async def stan_middleware( + request: "aiohttp.web.Request", + handler: Callable[..., object], + ) -> Awaitable["aiohttp.web.Response"]: try: - ctx = async_tracer.extract(opentracing.Format.HTTP_HEADERS, request.headers) - request['scope'] = async_tracer.start_active_span('aiohttp-server', child_of=ctx) - scope = request['scope'] + span_context = tracer.extract(Format.HTTP_HEADERS, request.headers) + span: "InstanaSpan" = tracer.start_span( + "aiohttp-server", span_context=span_context + ) + request["span"] = span # Query param scrubbing url = str(request.url) - parts = url.split('?') + parts = url.split("?") if len(parts) > 1: - cleaned_qp = strip_secrets_from_query(parts[1], - agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", cleaned_qp) + cleaned_qp = strip_secrets_from_query( + parts[1], agent.options.secrets_matcher, agent.options.secrets_list + ) + span.set_attribute("http.params", cleaned_qp) - scope.span.set_tag("http.url", parts[0]) - scope.span.set_tag("http.method", request.method) + span.set_attribute(SpanAttributes.HTTP_URL, parts[0]) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) # Custom header tracking support - if agent.options.extra_http_headers is not None: + if agent.options.extra_http_headers: for custom_header in agent.options.extra_http_headers: if custom_header in request.headers: - scope.span.set_tag("http.header.%s" % custom_header, request.headers[custom_header]) + span.set_attribute( + "http.header.%s" % custom_header, + request.headers[custom_header], + ) response = None try: @@ -52,33 +66,38 @@ async def stan_middleware(request, handler): if response is not None: # Mark 500 responses as errored if 500 <= response.status: - scope.span.mark_as_errored() + span.mark_as_errored() - scope.span.set_tag("http.status_code", response.status) - async_tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, response.headers) - response.headers['Server-Timing'] = "intid;desc=%s" % scope.span.context.trace_id + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers["Server-Timing"] = ( + f"intid;desc={span.context.trace_id}" + ) return response except Exception as exc: - logger.debug("aiohttp stan_middleware", exc_info=True) - if scope is not None: - scope.span.set_tag("http.status_code", 500) - scope.span.log_exception(exc) + logger.debug("aiohttp server stan_middleware:", exc_info=True) + if span: + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) + span.record_exception(exc) raise finally: - if scope is not None: - scope.close() - - - @wrapt.patch_function_wrapper('aiohttp.web', 'Application.__init__') - def init_with_instana(wrapped, instance, argv, kwargs): + if span and span.is_recording(): + span.end() + + @wrapt.patch_function_wrapper("aiohttp.web", "Application.__init__") + def init_with_instana( + wrapped: Callable[..., "aiohttp.web.Application.__init__"], + instance: "aiohttp.web.Application", + args: Tuple[int, str, Tuple[Any, ...]], + kwargs: Dict[str, Any], + ) -> object: if "middlewares" in kwargs: kwargs["middlewares"].insert(0, stan_middleware) else: kwargs["middlewares"] = [stan_middleware] - return wrapped(*argv, **kwargs) - + return wrapped(*args, **kwargs) logger.debug("Instrumenting aiohttp server") except ImportError: diff --git a/src/instana/propagators/http_propagator.py b/src/instana/propagators/http_propagator.py index b0326001..483f2765 100644 --- a/src/instana/propagators/http_propagator.py +++ b/src/instana/propagators/http_propagator.py @@ -53,8 +53,8 @@ def inject_key_value(carrier, key, value): if span_context.suppression: return - inject_key_value(carrier, self.HEADER_KEY_T, trace_id) - inject_key_value(carrier, self.HEADER_KEY_S, span_id) + inject_key_value(carrier, self.HEADER_KEY_T, str(trace_id)) + inject_key_value(carrier, self.HEADER_KEY_S, str(span_id)) except Exception: logger.debug("inject error:", exc_info=True) From 74745a0f25c9957adbc5d88492b90e00f383f06e Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Wed, 28 Aug 2024 11:24:11 +0200 Subject: [PATCH 150/172] tests(asyncio): adapt tests to OTel usage. Signed-off-by: Paulo Vital --- tests/conftest.py | 1 - tests/frameworks/test_asyncio.py | 129 +++++++++++++++++-------------- 2 files changed, 69 insertions(+), 61 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e5e49643..5841b14d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -45,7 +45,6 @@ collect_ignore_glob.append("*clients/test_sql*") collect_ignore_glob.append("*frameworks/test_aiohttp*") -collect_ignore_glob.append("*frameworks/test_asyncio*") collect_ignore_glob.append("*frameworks/test_celery*") collect_ignore_glob.append("*frameworks/test_gevent*") collect_ignore_glob.append("*frameworks/test_grpcio*") diff --git a/tests/frameworks/test_asyncio.py b/tests/frameworks/test_asyncio.py index 97483616..5a3fbe61 100644 --- a/tests/frameworks/test_asyncio.py +++ b/tests/frameworks/test_asyncio.py @@ -2,19 +2,37 @@ # (c) Copyright Instana Inc. 2020 import asyncio +from typing import Any, Dict, Generator, Optional + import aiohttp -import unittest +import pytest -import tests.apps.flask_app -from ..helpers import testenv +import tests.apps.flask_app # noqa: F401 from instana.configurator import config -from instana.singletons import async_tracer - +from instana.singletons import tracer +from tests.helpers import testenv + + +class TestAsyncio: + async def fetch( + self, + session: aiohttp.ClientSession, + url: str, + headers: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ): + try: + async with session.get(url, headers=headers, params=params) as response: + return response + except aiohttp.web_exceptions.HTTPException: + pass -class TestAsyncio(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = async_tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test @@ -22,120 +40,111 @@ def setUp(self): asyncio.set_event_loop(None) # Restore default - config['asyncio_task_context_propagation']['enabled'] = False - - def tearDown(self): - """ Purge the queue """ - pass - - async def fetch(self, session, url, headers=None): - try: - async with session.get(url, headers=headers) as response: - return response - except aiohttp.web_exceptions.HTTPException: - pass - - def test_ensure_future_with_context(self): + config["asyncio_task_context_propagation"]["enabled"] = False + yield + # teardown + # Close the loop if running + if self.loop.is_running(): + self.loop.close() + + def test_ensure_future_with_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.ensure_future(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.ensure_future(run_later("Hello OTel")) await asyncio.sleep(0.5) # Override default task context propagation - config['asyncio_task_context_propagation']['enabled'] = True + config["asyncio_task_context_propagation"]["enabled"] = True self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 test_span = spans[0] wsgi_span = spans[1] aioclient_span = spans[2] - self.assertEqual(test_span.t, wsgi_span.t) - self.assertEqual(test_span.t, aioclient_span.t) + assert test_span.t == wsgi_span.t + assert aioclient_span.t == test_span.t - self.assertEqual(test_span.p, None) - self.assertEqual(wsgi_span.p, aioclient_span.s) - self.assertEqual(aioclient_span.p, test_span.s) + assert not test_span.p + assert wsgi_span.p == aioclient_span.s + assert aioclient_span.p == test_span.s - def test_ensure_future_without_context(self): + def test_ensure_future_without_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.ensure_future(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.ensure_future(run_later("Hello OTel")) await asyncio.sleep(0.5) self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertEqual("sdk", spans[0].n) - self.assertEqual("wsgi", spans[1].n) + assert len(spans) == 2 + assert spans[0].n == "sdk" + assert spans[1].n == "wsgi" # Without the context propagated, we should get two separate traces - self.assertNotEqual(spans[0].t, spans[1].t) + assert spans[0].t != spans[1].t if hasattr(asyncio, "create_task"): - def test_create_task_with_context(self): + + def test_create_task_with_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.create_task(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.create_task(run_later("Hello OTel")) await asyncio.sleep(0.5) # Override default task context propagation - config['asyncio_task_context_propagation']['enabled'] = True + config["asyncio_task_context_propagation"]["enabled"] = True self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 test_span = spans[0] wsgi_span = spans[1] aioclient_span = spans[2] - self.assertEqual(test_span.t, wsgi_span.t) - self.assertEqual(test_span.t, aioclient_span.t) + assert wsgi_span.t == test_span.t + assert aioclient_span.t == test_span.t - self.assertEqual(test_span.p, None) - self.assertEqual(wsgi_span.p, aioclient_span.s) - self.assertEqual(aioclient_span.p, test_span.s) + assert not test_span.p + assert wsgi_span.p == aioclient_span.s + assert aioclient_span.p == test_span.s - def test_create_task_without_context(self): + def test_create_task_without_context(self) -> None: async def run_later(msg="Hello"): - # print("run_later: %s" % async_tracer.active_span.operation_name) async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") async def test(): - with async_tracer.start_active_span('test'): - asyncio.create_task(run_later("Hello")) + with tracer.start_as_current_span("test"): + asyncio.create_task(run_later("Hello OTel")) await asyncio.sleep(0.5) self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - self.assertEqual("sdk", spans[0].n) - self.assertEqual("wsgi", spans[1].n) + assert len(spans) == 2 + assert spans[0].n == "sdk" + assert spans[1].n == "wsgi" # Without the context propagated, we should get two separate traces - self.assertNotEqual(spans[0].t, spans[1].t) + assert spans[0].t != spans[1].t From 2b4a16f4ae5ed1b4b5ee8ca663d6c00034a19c8d Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Fri, 23 Aug 2024 14:27:53 +0200 Subject: [PATCH 151/172] tests(aiohttp): adapt tests to OTel usage. Signed-off-by: Paulo Vital --- tests/apps/aiohttp_app2/__init__.py | 13 + tests/apps/aiohttp_app2/app.py | 40 ++ tests/conftest.py | 1 - tests/frameworks/test_aiohttp_client.py | 618 ++++++++++++------------ tests/frameworks/test_aiohttp_server.py | 578 +++++++++++----------- 5 files changed, 644 insertions(+), 606 deletions(-) create mode 100644 tests/apps/aiohttp_app2/__init__.py create mode 100644 tests/apps/aiohttp_app2/app.py diff --git a/tests/apps/aiohttp_app2/__init__.py b/tests/apps/aiohttp_app2/__init__.py new file mode 100644 index 00000000..e382343a --- /dev/null +++ b/tests/apps/aiohttp_app2/__init__.py @@ -0,0 +1,13 @@ +# (c) Copyright IBM Corp. 2024 + +import os +import sys +from tests.apps.aiohttp_app2.app import aiohttp_server as server +from tests.apps.utils import launch_background_thread + +APP_THREAD = None + +if not any((os.environ.get('GEVENT_STARLETTE_TEST'), + os.environ.get('CASSANDRA_TEST'), + sys.version_info < (3, 5, 3))): + APP_THREAD = launch_background_thread(server, "AIOHTTP") diff --git a/tests/apps/aiohttp_app2/app.py b/tests/apps/aiohttp_app2/app.py new file mode 100644 index 00000000..82b3d24c --- /dev/null +++ b/tests/apps/aiohttp_app2/app.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) Copyright IBM Corp. 2024 + +import asyncio + +from aiohttp import web + +from tests.helpers import testenv + +testenv["aiohttp_port"] = 10810 +testenv["aiohttp_server"] = f"http://127.0.0.1:{testenv['aiohttp_port']}" + + +def say_hello(request): + return web.Response(text="Hello, world") + + +@web.middleware +async def middleware1(request, handler): + print("Middleware 1 called") + response = await handler(request) + print("Middleware 1 finished") + return response + + +def aiohttp_server(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + app = web.Application(middlewares=[middleware1]) + app.add_routes([web.get("/", say_hello)]) + + runner = web.AppRunner(app) + loop.run_until_complete(runner.setup()) + site = web.TCPSite(runner, "127.0.0.1", testenv["aiohttp_port"]) + + loop.run_until_complete(site.start()) + loop.run_forever() diff --git a/tests/conftest.py b/tests/conftest.py index 5841b14d..b73954b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,7 +44,6 @@ collect_ignore_glob.append("*clients/test_redis*") collect_ignore_glob.append("*clients/test_sql*") -collect_ignore_glob.append("*frameworks/test_aiohttp*") collect_ignore_glob.append("*frameworks/test_celery*") collect_ignore_glob.append("*frameworks/test_gevent*") collect_ignore_glob.append("*frameworks/test_grpcio*") diff --git a/tests/frameworks/test_aiohttp_client.py b/tests/frameworks/test_aiohttp_client.py index 72efc437..f1231fa8 100644 --- a/tests/frameworks/test_aiohttp_client.py +++ b/tests/frameworks/test_aiohttp_client.py @@ -1,91 +1,98 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 +from typing import Any, Dict, Generator, Optional import aiohttp import asyncio -import unittest -from instana.singletons import async_tracer, agent +import pytest -import tests.apps.flask_app -import tests.apps.aiohttp_app -from ..helpers import testenv +from instana.singletons import tracer, agent +import tests.apps.flask_app # noqa: F401 +import tests.apps.aiohttp_app # noqa: F401 +from tests.helpers import testenv -class TestAiohttp(unittest.TestCase): - async def fetch(self, session, url, headers=None, params=None): +class TestAiohttpClient: + async def fetch( + self, + session: aiohttp.client.ClientSession, + url: str, + headers: Optional[Dict[str, Any]] = None, + params: Optional[Dict[str, Any]] = None, + ): try: async with session.get(url, headers=headers, params=params) as response: return response except aiohttp.web_exceptions.HTTPException: pass - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = async_tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + yield + # teardown + # Ensure that allow_exit_as_root has the default value""" agent.options.allow_exit_as_root = False - def test_client_get(self): + def test_client_get(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_as_root_exit_span(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True + async def test(): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/") @@ -93,367 +100,339 @@ async def test(): response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 wsgi_span = spans[0] aiohttp_span = spans[1] - self.assertIsNone(async_tracer.active_span) - - self.assertEqual(aiohttp_span.t, wsgi_span.t) - # Same traceId - traceId = aiohttp_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == wsgi_span.t # Parent relationships - self.assertIsNone(aiohttp_span.p) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert not aiohttp_span.p + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_301(self): + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(wsgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={wsgi_span.t}" + + def test_client_get_301(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/301") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 wsgi_span1 = spans[0] wsgi_span2 = spans[1] aiohttp_span = spans[2] test_span = spans[3] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span1.t) - self.assertEqual(traceId, wsgi_span2.t) + assert aiohttp_span.t == traceId + assert wsgi_span1.t == traceId + assert wsgi_span2.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span1.p, aiohttp_span.s) - self.assertEqual(wsgi_span2.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span1.p == aiohttp_span.s + assert wsgi_span2.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span1.ec) - self.assertIsNone(wsgi_span2.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/301", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span2.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_405(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span1.ec + assert not wsgi_span2.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/301" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span2.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_405(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/405") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(405, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/405", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_500(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 405 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/405" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_500(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/500") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(wsgi_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(500, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/500", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual('INTERNAL SERVER ERROR', - aiohttp_span.data["http"]["error"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_504(self): + assert not test_span.ec + assert aiohttp_span.ec == 1 + assert wsgi_span.ec == 1 + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 500 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/500" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["error"] == "INTERNAL SERVER ERROR" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_504(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["flask_server"] + "/504") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(wsgi_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(504, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/504", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual('GATEWAY TIMEOUT', aiohttp_span.data["http"]["error"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_with_params_to_scrub(self): + assert not test_span.ec + assert aiohttp_span.ec == 1 + assert wsgi_span.ec == 1 + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 504 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/504" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["error"] == "GATEWAY TIMEOUT" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_get_with_params_to_scrub(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["flask_server"], params={"secret": "yeah"}) + return await self.fetch( + session, testenv["flask_server"], params={"secret": "yeah"} + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual("secret=", - aiohttp_span.data["http"]["params"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_response_header_capture(self): + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["params"] == "secret=" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + def test_client_response_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This'] + agent.options.extra_http_headers = ["X-Capture-This"] async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["flask_server"] + "/response_headers") + return await self.fetch( + session, testenv["flask_server"] + "/response_headers" + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 wsgi_span = spans[0] aiohttp_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["flask_server"] + "/response_headers", aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIn("X-Capture-This", aiohttp_span.data["http"]["header"]) - self.assertEqual("Ok", aiohttp_span.data["http"]["header"]["X-Capture-This"]) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], wsgi_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual(response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/response_headers" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-Capture-This" in aiohttp_span.data["http"]["header"] + assert aiohttp_span.data["http"]["header"]["X-Capture-This"] == "Ok" + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" agent.options.extra_http_headers = original_extra_http_headers - def test_client_error(self): + def test_client_error(self) -> None: async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, 'http://doesnotexist:10/') + return await self.fetch(session, "http://doesnotexist:10/") response = None try: @@ -462,33 +441,62 @@ async def test(): pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 aiohttp_span = spans[0] test_span = spans[1] - self.assertIsNone(async_tracer.active_span) - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) + assert aiohttp_span.t == test_span.t # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) + assert aiohttp_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertIsNone(aiohttp_span.data["http"]["status"]) - self.assertEqual("http://doesnotexist:10/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.data["http"]["error"]) - self.assertTrue(len(aiohttp_span.data["http"]["error"])) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIsNone(response) + assert test_span.ec + assert aiohttp_span.ec == 1 + + assert aiohttp_span.n == "aiohttp-client" + assert not aiohttp_span.data["http"]["status"] + assert aiohttp_span.data["http"]["url"] == "http://doesnotexist:10/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.data["http"]["error"] + assert len(aiohttp_span.data["http"]["error"]) + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert not response + + def test_client_get_tracing_off(self, mocker) -> None: + mocker.patch( + "instana.instrumentation.aiohttp.client.tracing_is_off", + return_value=True, + ) + + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["flask_server"] + "/") + + response = self.loop.run_until_complete(test()) + assert response.status == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + # Span names are not "aiohttp-client" + for span in spans: + assert span.n != "aiohttp-client" + + def test_client_get_provided_tracing_config(self, mocker) -> None: + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession(trace_configs=[]) as session: + return await self.fetch(session, testenv["flask_server"] + "/") + + response = self.loop.run_until_complete(test()) + assert response.status == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 diff --git a/tests/frameworks/test_aiohttp_server.py b/tests/frameworks/test_aiohttp_server.py index 41dd2ce8..aa4b15e3 100644 --- a/tests/frameworks/test_aiohttp_server.py +++ b/tests/frameworks/test_aiohttp_server.py @@ -1,18 +1,17 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import aiohttp import asyncio -import unittest - -import tests.apps.aiohttp_app -from ..helpers import testenv +from typing import Generator -from instana.singletons import async_tracer, agent +import aiohttp +import pytest +from instana.singletons import agent, tracer +from tests.helpers import testenv -class TestAiohttpServer(unittest.TestCase): +class TestAiohttpServer: async def fetch(self, session, url, headers=None, params=None): try: async with session.get(url, headers=headers, params=params) as response: @@ -20,461 +19,440 @@ async def fetch(self, session, url, headers=None, params=None): except aiohttp.web_exceptions.HTTPException: pass - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = async_tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Load test server application + import tests.apps.aiohttp_app # noqa: F401 + + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() # New event loop for every test self.loop = asyncio.new_event_loop() asyncio.set_event_loop(None) - - def tearDown(self): - pass + yield def test_server_get(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Synthetic - self.assertIsNone(test_span.sy) - self.assertIsNone(aioclient_span.sy) - self.assertIsNone(aioserver_span.sy) + assert not test_span.sy + assert not aioclient_span.sy + assert not aioserver_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(200, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(200, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 200 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_get_204(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/204") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId trace_id = test_span.t - self.assertEqual(trace_id, aioclient_span.t) - self.assertEqual(trace_id, aioserver_span.t) + assert aioclient_span.t == trace_id + assert aioserver_span.t == trace_id # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Synthetic - self.assertIsNone(test_span.sy) - self.assertIsNone(aioclient_span.sy) - self.assertIsNone(aioserver_span.sy) + assert not test_span.sy + assert not aioclient_span.sy + assert not aioserver_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(204, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/204", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(204, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/204", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(isinstance(aioclient_span.stack, list)) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], trace_id) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % trace_id) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 204 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/204" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(trace_id) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={trace_id}" def test_server_synthetic_request(self): async def test(): - headers = { - 'X-INSTANA-SYNTHETIC': '1' - } + headers = {"X-INSTANA-SYNTHETIC": "1"} - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["aiohttp_server"] + "/", headers=headers) + return await self.fetch( + session, testenv["aiohttp_server"] + "/", headers=headers + ) response = self.loop.run_until_complete(test()) + assert response spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 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) + assert aioserver_span.sy + assert not aioclient_span.sy + assert not test_span.sy def test_server_get_with_params_to_scrub(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["aiohttp_server"], params={"secret": "iloveyou"}) + return await self.fetch( + session, + testenv["aiohttp_server"], + params={"secret": "iloveyou"}, + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(200, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertEqual("secret=", - aioserver_span.data["http"]["params"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(200, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual("secret=", - aioclient_span.data["http"]["params"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 200 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/" + assert aioserver_span.data["http"]["method"] == "GET" + assert aioserver_span.data["http"]["params"] == "secret=" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_custom_header_capture(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: # Hack together a manual custom headers list agent.options.extra_http_headers = [ - u'X-Capture-This', u'X-Capture-That'] + "X-Capture-This", + "X-Capture-That", + ] headers = dict() - headers['X-Capture-This'] = 'this' - headers['X-Capture-That'] = 'that' + headers["X-Capture-This"] = "this" + headers["X-Capture-That"] = "that" - return await self.fetch(session, testenv["aiohttp_server"], headers=headers, params={"secret": "iloveyou"}) + return await self.fetch( + session, + testenv["aiohttp_server"], + headers=headers, + params={"secret": "iloveyou"}, + ) response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(200, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertEqual("secret=", - aioserver_span.data["http"]["params"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(200, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual("secret=", - aioclient_span.data["http"]["params"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - self.assertIn("X-Capture-This", aioserver_span.data["http"]["header"]) - self.assertEqual("this", aioserver_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", aioserver_span.data["http"]["header"]) - self.assertEqual("that", aioserver_span.data["http"]["header"]["X-Capture-That"]) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 200 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/" + assert aioserver_span.data["http"]["method"] == "GET" + assert aioserver_span.data["http"]["params"] == "secret=" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" + + assert "X-Capture-This" in aioserver_span.data["http"]["header"] + assert aioserver_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in aioserver_span.data["http"]["header"] + assert aioserver_span.data["http"]["header"]["X-Capture-That"] == "that" def test_server_get_401(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/401") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aioclient_span.ec) - self.assertIsNone(aioserver_span.ec) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(401, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/401", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(401, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/401", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 401 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/401" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_get_500(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: return await self.fetch(session, testenv["aiohttp_server"] + "/500") response = self.loop.run_until_complete(test()) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aioclient_span.ec, 1) - self.assertEqual(aioserver_span.ec, 1) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(500, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/500", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(500, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/500", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual('I must simulate errors.', - aioclient_span.data["http"]["error"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) - - self.assertIn("X-INSTANA-T", response.headers) - self.assertEqual(response.headers["X-INSTANA-T"], traceId) - self.assertIn("X-INSTANA-S", response.headers) - self.assertEqual(response.headers["X-INSTANA-S"], aioserver_span.s) - self.assertIn("X-INSTANA-L", response.headers) - self.assertEqual(response.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", response.headers) - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) + assert not test_span.ec + assert aioclient_span.ec == 1 + assert aioserver_span.ec == 1 + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 500 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/500" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={traceId}" def test_server_get_exception(self): async def test(): - with async_tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["aiohttp_server"] + "/exception") + return await self.fetch( + session, testenv["aiohttp_server"] + "/exception" + ) response = self.loop.run_until_complete(test()) + assert response spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 aioserver_span = spans[0] aioclient_span = spans[1] test_span = spans[2] - self.assertIsNone(async_tracer.active_span) - # Same traceId traceId = test_span.t - self.assertEqual(traceId, aioclient_span.t) - self.assertEqual(traceId, aioserver_span.t) + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId # Parent relationships - self.assertEqual(aioclient_span.p, test_span.s) - self.assertEqual(aioserver_span.p, aioclient_span.s) + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aioclient_span.ec, 1) - self.assertEqual(aioserver_span.ec, 1) - - self.assertEqual("aiohttp-server", aioserver_span.n) - self.assertEqual(500, aioserver_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/exception", aioserver_span.data["http"]["url"]) - self.assertEqual("GET", aioserver_span.data["http"]["method"]) - self.assertIsNone(aioserver_span.stack) - - self.assertEqual("aiohttp-client", aioclient_span.n) - self.assertEqual(500, aioclient_span.data["http"]["status"]) - self.assertEqual(testenv["aiohttp_server"] + - "/exception", aioclient_span.data["http"]["url"]) - self.assertEqual("GET", aioclient_span.data["http"]["method"]) - self.assertEqual('Internal Server Error', - aioclient_span.data["http"]["error"]) - self.assertIsNotNone(aioclient_span.stack) - self.assertTrue(type(aioclient_span.stack) is list) - self.assertTrue(len(aioclient_span.stack) > 1) + assert not test_span.ec + assert aioclient_span.ec == 1 + assert aioserver_span.ec == 1 + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 500 + assert ( + aioserver_span.data["http"]["url"] + == f"{testenv['aiohttp_server']}/exception" + ) + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert aioclient_span.n == "aiohttp-client" + assert aioclient_span.data["http"]["status"] == 500 + assert aioclient_span.data["http"]["error"] == "Internal Server Error" + assert aioclient_span.stack + assert isinstance(aioclient_span.stack, list) + assert len(aioclient_span.stack) > 1 + + +class TestAiohttpServerMiddleware: + async def fetch(self, session, url, headers=None, params=None): + try: + async with session.get(url, headers=headers, params=params) as response: + return response + except aiohttp.web_exceptions.HTTPException: + pass + + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Load test server application + import tests.apps.aiohttp_app2 # noqa: F401 + + # Clear all spans before a test run + self.recorder = tracer.span_processor + self.recorder.clear_spans() + + # New event loop for every test + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + yield + + def test_server_get(self): + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["aiohttp_server"] + "/") + + response = self.loop.run_until_complete(test()) + assert response + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + aioserver_span = spans[0] + aioclient_span = spans[1] + test_span = spans[2] + + # Same traceId + traceId = test_span.t + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId + + # Parent relationships + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s From 4637b94b0be63851730e086605234bb93fd4a9e8 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 9 Sep 2024 13:58:59 +0530 Subject: [PATCH 152/172] feat: enable auto-instrumentation with middleware for sanic Signed-off-by: Varsha GS (cherry picked from commit fb29c7931675f8508743917f7188eab199dca9f5) --- src/instana/__init__.py | 2 +- src/instana/instrumentation/sanic_inst.py | 228 ++++++++++------------ src/instana/util/traceutils.py | 2 +- 3 files changed, 108 insertions(+), 124 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 7ecf67a1..df4ce2c8 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -181,7 +181,7 @@ def boot_agent(): # redis, # noqa: F401 # sqlalchemy, # noqa: F401 starlette_inst, # noqa: F401 - # sanic_inst, # noqa: F401 + sanic_inst, # noqa: F401 urllib3, # noqa: F401 ) from instana.instrumentation.aiohttp import ( diff --git a/src/instana/instrumentation/sanic_inst.py b/src/instana/instrumentation/sanic_inst.py index 39d44549..ef4eacb6 100644 --- a/src/instana/instrumentation/sanic_inst.py +++ b/src/instana/instrumentation/sanic_inst.py @@ -1,5 +1,4 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2021 +# (c) Copyright IBM Corp. 2024 """ Instrumentation for Sanic @@ -8,112 +7,59 @@ try: import sanic import wrapt - import opentracing - from ..log import logger - from ..singletons import async_tracer, agent - from ..util.secrets import strip_secrets_from_query - from ..util.traceutils import extract_custom_headers - - - @wrapt.patch_function_wrapper('sanic.exceptions', 'SanicException.__init__') - def exception_with_instana(wrapped, instance, args, kwargs): - try: - message = kwargs.get("message") or args[0] - status_code = kwargs.get("status_code") - span = async_tracer.active_span - - if all([span, status_code, message]) and 500 <= status_code: - span.set_tag("http.error", message) - try: - wrapped(*args, **kwargs) - except Exception as exc: - span.log_exception(exc) - else: - wrapped(*args, **kwargs) - except Exception: - logger.debug("exception_with_instana: ", exc_info=True) - wrapped(*args, **kwargs) - - - def response_details(span, response): - try: - status_code = response.status - if status_code is not None: - if 500 <= int(status_code): - span.mark_as_errored() - span.set_tag('http.status_code', status_code) - - if response.headers is not None: - extract_custom_headers(span, response.headers) - async_tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, response.headers) - response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id - except Exception: - logger.debug("send_wrapper: ", exc_info=True) - - - if hasattr(sanic.response.BaseHTTPResponse, "send"): - @wrapt.patch_function_wrapper('sanic.response', 'BaseHTTPResponse.send') - async def send_with_instana(wrapped, instance, args, kwargs): - span = async_tracer.active_span - if span is None: - await wrapped(*args, **kwargs) - else: - response_details(span=span, response=instance) - try: - await wrapped(*args, **kwargs) - except Exception as exc: - span.log_exception(exc) - raise + from typing import Callable, Tuple, Dict, Any + from sanic.exceptions import SanicException + + from opentelemetry import context, trace + from opentelemetry.trace import SpanKind + from opentelemetry.semconv.trace import SpanAttributes + + from instana.log import logger + from instana.singletons import tracer, agent + from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import extract_custom_headers + from instana.propagators.format import Format + + if hasattr(sanic.request, "types"): + from sanic.request.types import Request + from sanic.response.types import HTTPResponse else: - @wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.write_response') - def write_with_instana(wrapped, instance, args, kwargs): - response = args[0] - span = async_tracer.active_span - if span is None: - wrapped(*args, **kwargs) - else: - response_details(span=span, response=response) - try: - wrapped(*args, **kwargs) - except Exception as exc: - span.log_exception(exc) - raise - - - @wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.stream_response') - async def stream_with_instana(wrapped, instance, args, kwargs): - response = args[0] - span = async_tracer.active_span - if span is None: - await wrapped(*args, **kwargs) - else: - response_details(span=span, response=response) - try: - await wrapped(*args, **kwargs) - except Exception as exc: - span.log_exception(exc) - raise - - - @wrapt.patch_function_wrapper('sanic.app', 'Sanic.handle_request') - async def handle_request_with_instana(wrapped, instance, args, kwargs): - - try: - request = args[0] - try: # scheme attribute is calculated in the sanic handle_request method for v19, not yet present + from sanic.request import Request + from sanic.response import HTTPResponse + + + @wrapt.patch_function_wrapper("sanic.app", "Sanic.__init__") + def init_with_instana( + wrapped: Callable[..., sanic.app.Sanic.__init__], + instance: sanic.app.Sanic, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> None: + wrapped(*args, **kwargs) + app = instance + + @app.middleware("request") + def request_with_instana(request: Request) -> None: + try: if "http" not in request.scheme: - return await wrapped(*args, **kwargs) - except AttributeError: - pass - headers = request.headers.copy() - ctx = async_tracer.extract(opentracing.Format.HTTP_HEADERS, headers) - with async_tracer.start_active_span("asgi", child_of=ctx) as scope: - scope.span.set_tag('span.kind', 'entry') - scope.span.set_tag('http.path', request.path) - scope.span.set_tag('http.method', request.method) - scope.span.set_tag('http.host', request.host) + return + + headers = request.headers.copy() + parent_context = tracer.extract(Format.HTTP_HEADERS, headers) + + span = tracer.start_span("asgi", span_context=parent_context) + request.ctx.span = span + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + request.ctx.token = token + + span.set_attribute('span.kind', SpanKind.CLIENT) + span.set_attribute('http.path', request.path) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) + span.set_attribute(SpanAttributes.HTTP_HOST, request.host) if hasattr(request, "url"): - scope.span.set_tag("http.url", request.url) + span.set_attribute(SpanAttributes.HTTP_URL, request.url) query = request.query_string @@ -121,24 +67,62 @@ async def handle_request_with_instana(wrapped, instance, args, kwargs): if isinstance(query, bytes): query = query.decode('utf-8') scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", scrubbed_params) + agent.options.secrets_list) + span.set_attribute("http.params", scrubbed_params) - if agent.options.extra_http_headers is not None: - extract_custom_headers(scope.span, headers) - await wrapped(*args, **kwargs) + if agent.options.extra_http_headers: + extract_custom_headers(span, headers) if hasattr(request, "uri_template") and request.uri_template: - scope.span.set_tag("http.path_tpl", request.uri_template) - if hasattr(request, "ctx"): # ctx attribute added in the latest v19 versions - request.ctx.iscope = scope - except Exception as e: - logger.debug("Sanic framework @ handle_request", exc_info=True) - return await wrapped(*args, **kwargs) - - - logger.debug("Instrumenting Sanic") + span.set_attribute("http.path_tpl", request.uri_template) + except Exception: + logger.debug("request_with_instana: ", exc_info=True) + + + @app.exception(Exception) + def exception_with_instana(request: Request, exception: Exception) -> None: + try: + if not hasattr(request.ctx, "span"): + return + span = request.ctx.span + + if isinstance(exception, SanicException): + # Handle Sanic-specific exceptions + status_code = exception.status_code + message = str(exception) + + if all([span, status_code, message]) and 500 <= status_code: + span.set_attribute("http.error", message) + except Exception: + logger.debug("exception_with_instana: ", exc_info=True) + + + @app.middleware("response") + def response_with_instana(request: Request, response: HTTPResponse) -> None: + try: + if not hasattr(request.ctx, "span"): + return + span = request.ctx.span + + status_code = response.status + if status_code: + if int(status_code) >= 500: + span.mark_as_errored() + span.set_attribute('http.status_code', status_code) + + if hasattr(response, "headers"): + extract_custom_headers(span, response.headers) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id + + if span.is_recording(): + span.end() + request.ctx.span = None + + if request.ctx.token: + context.detach(request.ctx.token) + request.ctx.token = None + except Exception: + logger.debug("response_with_instana: ", exc_info=True) except ImportError: - pass -except AttributeError: - logger.debug("Not supported Sanic version") + pass \ No newline at end of file diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index c21b058b..edcba787 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -15,7 +15,7 @@ def extract_custom_headers(tracing_span, headers) -> None: # Headers are in the following format: b'x-header-1' for header_key, value in headers.items(): if header_key.lower() == custom_header.lower(): - tracing_span.set_tag("http.header.%s" % custom_header, value) + tracing_span.set_attribute("http.header.%s" % custom_header, value) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) From 2fd617614c49fb30db49ed2aeb20c8b58397cbc1 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Wed, 11 Sep 2024 11:03:22 +0530 Subject: [PATCH 153/172] sanic: refactor tests Signed-off-by: Varsha GS (cherry picked from commit b4968d031793fbf76b15b6d1456f45c54f0c744c) --- src/instana/instrumentation/sanic_inst.py | 25 +- tests/apps/sanic_app/__init__.py | 5 +- tests/apps/sanic_app/name.py | 3 - tests/apps/sanic_app/server.py | 24 +- tests/apps/sanic_app/simpleview.py | 24 +- tests/conftest.py | 1 - tests/frameworks/test_sanic.py | 737 +++++++++++----------- tests/requirements-310.txt | 2 +- tests/requirements-312.txt | 2 +- tests/requirements-313.txt | 5 +- tests/requirements.txt | 2 +- tests/test_utils.py | 8 +- 12 files changed, 429 insertions(+), 409 deletions(-) diff --git a/src/instana/instrumentation/sanic_inst.py b/src/instana/instrumentation/sanic_inst.py index ef4eacb6..6d50b4b5 100644 --- a/src/instana/instrumentation/sanic_inst.py +++ b/src/instana/instrumentation/sanic_inst.py @@ -4,6 +4,7 @@ Instrumentation for Sanic https://sanicframework.org/en/ """ + try: import sanic import wrapt @@ -27,7 +28,6 @@ from sanic.request import Request from sanic.response import HTTPResponse - @wrapt.patch_function_wrapper("sanic.app", "Sanic.__init__") def init_with_instana( wrapped: Callable[..., sanic.app.Sanic.__init__], @@ -43,7 +43,7 @@ def request_with_instana(request: Request) -> None: try: if "http" not in request.scheme: return - + headers = request.headers.copy() parent_context = tracer.extract(Format.HTTP_HEADERS, headers) @@ -54,8 +54,8 @@ def request_with_instana(request: Request) -> None: token = context.attach(ctx) request.ctx.token = token - span.set_attribute('span.kind', SpanKind.CLIENT) - span.set_attribute('http.path', request.path) + span.set_attribute("span.kind", SpanKind.CLIENT) + span.set_attribute("http.path", request.path) span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) span.set_attribute(SpanAttributes.HTTP_HOST, request.host) if hasattr(request, "url"): @@ -65,9 +65,10 @@ def request_with_instana(request: Request) -> None: if isinstance(query, (str, bytes)) and len(query): if isinstance(query, bytes): - query = query.decode('utf-8') - scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, - agent.options.secrets_list) + query = query.decode("utf-8") + scrubbed_params = strip_secrets_from_query( + query, agent.options.secrets_matcher, agent.options.secrets_list + ) span.set_attribute("http.params", scrubbed_params) if agent.options.extra_http_headers: @@ -77,7 +78,6 @@ def request_with_instana(request: Request) -> None: except Exception: logger.debug("request_with_instana: ", exc_info=True) - @app.exception(Exception) def exception_with_instana(request: Request, exception: Exception) -> None: try: @@ -95,7 +95,6 @@ def exception_with_instana(request: Request, exception: Exception) -> None: except Exception: logger.debug("exception_with_instana: ", exc_info=True) - @app.middleware("response") def response_with_instana(request: Request, response: HTTPResponse) -> None: try: @@ -107,12 +106,14 @@ def response_with_instana(request: Request, response: HTTPResponse) -> None: if status_code: if int(status_code) >= 500: span.mark_as_errored() - span.set_attribute('http.status_code', status_code) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) if hasattr(response, "headers"): extract_custom_headers(span, response.headers) tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) - response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id + response.headers["Server-Timing"] = ( + "intid;desc=%s" % span.context.trace_id + ) if span.is_recording(): span.end() @@ -125,4 +126,4 @@ def response_with_instana(request: Request, response: HTTPResponse) -> None: logger.debug("response_with_instana: ", exc_info=True) except ImportError: - pass \ No newline at end of file + pass diff --git a/tests/apps/sanic_app/__init__.py b/tests/apps/sanic_app/__init__.py index a9daa911..f96b079b 100644 --- a/tests/apps/sanic_app/__init__.py +++ b/tests/apps/sanic_app/__init__.py @@ -4,11 +4,10 @@ import uvicorn -from ...helpers import testenv -from instana.log import logger +from tests.helpers import testenv testenv["sanic_port"] = 1337 -testenv["sanic_server"] = ("http://127.0.0.1:" + str(testenv["sanic_port"])) +testenv["sanic_server"] = f"http://127.0.0.1:{testenv['sanic_port']}" def launch_sanic(): diff --git a/tests/apps/sanic_app/name.py b/tests/apps/sanic_app/name.py index 055f5189..0838d29a 100644 --- a/tests/apps/sanic_app/name.py +++ b/tests/apps/sanic_app/name.py @@ -7,8 +7,5 @@ class NameView(HTTPMethodView): - def get(self, request, name): return text("Hello {}".format(name)) - - diff --git a/tests/apps/sanic_app/server.py b/tests/apps/sanic_app/server.py index 9c290f38..a07dafc9 100644 --- a/tests/apps/sanic_app/server.py +++ b/tests/apps/sanic_app/server.py @@ -10,32 +10,35 @@ from tests.apps.sanic_app.simpleview import SimpleView from tests.apps.sanic_app.name import NameView -app = Sanic('test') +app = Sanic("test") + @app.get("/foo/") async def uuid_handler(request, foo_id: int): return text("INT - {}".format(foo_id)) + @app.route("/response_headers") async def response_headers(request): - headers = { - 'X-Capture-This-Too': 'this too', - 'X-Capture-That-Too': 'that too' - } + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} return text("Stan wuz here with headers!", headers=headers) + @app.route("/test_request_args") -async def test_request_args(request): +async def test_request_args_500(request): raise SanicException("Something went wrong.", status_code=500) + @app.route("/instana_exception") -async def test_request_args(request): +async def test_instana_exception(request): raise SanicException(description="Something went wrong.", status_code=500) + @app.route("/wrong") -async def test_request_args(request): +async def test_request_args_400(request): raise SanicException(message="Something went wrong.", status_code=400) + @app.get("/tag/") async def tag_handler(request, tag): return text("Tag - {}".format(tag)) @@ -45,8 +48,5 @@ async def tag_handler(request, tag): app.add_route(NameView.as_view(), "/") -if __name__ == '__main__': +if __name__ == "__main__": app.run(host="0.0.0.0", port=8000, debug=True, access_log=True) - - - diff --git a/tests/apps/sanic_app/simpleview.py b/tests/apps/sanic_app/simpleview.py index 646a310d..8529ecdd 100644 --- a/tests/apps/sanic_app/simpleview.py +++ b/tests/apps/sanic_app/simpleview.py @@ -5,20 +5,20 @@ from sanic.views import HTTPMethodView from sanic.response import text -class SimpleView(HTTPMethodView): - def get(self, request): - return text("I am get method") +class SimpleView(HTTPMethodView): + def get(self, request): + return text("I am get method") - # You can also use async syntax - async def post(self, request): - return text("I am post method") + # You can also use async syntax + async def post(self, request): + return text("I am post method") - def put(self, request): - return text("I am put method") + def put(self, request): + return text("I am put method") - def patch(self, request): - return text("I am patch method") + def patch(self, request): + return text("I am patch method") - def delete(self, request): - return text("I am delete method") + def delete(self, request): + return text("I am delete method") diff --git a/tests/conftest.py b/tests/conftest.py index b73954b1..94fac26c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,7 +48,6 @@ collect_ignore_glob.append("*frameworks/test_gevent*") collect_ignore_glob.append("*frameworks/test_grpcio*") collect_ignore_glob.append("*frameworks/test_pyramid*") -collect_ignore_glob.append("*frameworks/test_sanic*") collect_ignore_glob.append("*frameworks/test_tornado*") # # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will diff --git a/tests/frameworks/test_sanic.py b/tests/frameworks/test_sanic.py index 93de3cd0..923e0e1d 100644 --- a/tests/frameworks/test_sanic.py +++ b/tests/frameworks/test_sanic.py @@ -4,476 +4,501 @@ import time import requests import multiprocessing -import unittest +import pytest +from typing import Generator from instana.singletons import tracer -from ..helpers import testenv -from ..helpers import get_first_span_by_filter -from ..test_utils import _TraceContextMixin - - -class TestSanic(unittest.TestCase, _TraceContextMixin): - def setUp(self): +from tests.helpers import testenv +from tests.helpers import get_first_span_by_filter +from tests.test_utils import _TraceContextMixin + + +class TestSanic(_TraceContextMixin): + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Setup and Teardown""" + # Clear all spans before a test run + self.recorder = tracer.span_processor + self.recorder.clear_spans() from tests.apps.sanic_app import launch_sanic + self.proc = multiprocessing.Process(target=launch_sanic, args=(), daemon=True) self.proc.start() time.sleep(2) - - def tearDown(self): - """ Kill server after tests """ + yield + # Kill server after tests self.proc.kill() - def test_vanilla_get(self): - result = requests.get(testenv["sanic_server"] + '/') + def test_vanilla_get(self) -> None: + result = requests.get(testenv["sanic_server"] + "/") - self.assertEqual(result.status_code, 200) - self.assertIn("X-INSTANA-T", result.headers) - self.assertIn("X-INSTANA-S", result.headers) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], "1") - self.assertIn("Server-Timing", result.headers) - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 1) - self.assertEqual(spans[0].n, 'asgi') + assert result.status_code == 200 + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + spans = self.recorder.queued_spans() + assert len(spans) == 1 + assert spans[0].n == "asgi" - def test_basic_get(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/') + def test_basic_get(self) -> None: + with tracer.start_as_current_span("test"): + result = requests.get(testenv["sanic_server"] + "/") - self.assertEqual(result.status_code, 200) + assert result.status_code == 200 - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) + spans = self.recorder.queued_spans() + assert len(spans) == 3 - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_404(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/foo/not_an_int') - - self.assertEqual(result.status_code, 404) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_404(self) -> None: + with tracer.start_as_current_span("test"): + result = requests.get(testenv["sanic_server"] + "/foo/not_an_int") + + assert result.status_code == 404 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/foo/not_an_int') - self.assertIsNone(asgi_span.data['http']['path_tpl']) - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 404) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_sanic_exception(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/wrong') - - self.assertEqual(result.status_code, 400) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/foo/not_an_int" + assert not asgi_span.data["http"]["path_tpl"] + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 404 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_sanic_exception(self) -> None: + with tracer.start_as_current_span("test"): + result = requests.get(testenv["sanic_server"] + "/wrong") + + assert result.status_code == 400 + + spans = self.recorder.queued_spans() + assert len(spans) == 4 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/wrong') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/wrong') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 400) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_500_instana_exception(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/instana_exception') - - self.assertEqual(result.status_code, 500) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 4) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/wrong" + assert asgi_span.data["http"]["path_tpl"] == "/wrong" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 400 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_500_instana_exception(self) -> None: + with tracer.start_as_current_span("test"): + result = requests.get(testenv["sanic_server"] + "/instana_exception") + + assert result.status_code == 500 + + spans = self.recorder.queued_spans() + assert len(spans) == 4 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertEqual(asgi_span.ec, 1) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/instana_exception') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/instana_exception') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 500) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_500(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/test_request_args') - - self.assertEqual(result.status_code, 500) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 4) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/instana_exception" + assert asgi_span.data["http"]["path_tpl"] == "/instana_exception" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_500(self) -> None: + with tracer.start_as_current_span("test"): + result = requests.get(testenv["sanic_server"] + "/test_request_args") + + assert result.status_code == 500 + + spans = self.recorder.queued_spans() + assert len(spans) == 4 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertEqual(asgi_span.ec, 1) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/test_request_args') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/test_request_args') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 500) - self.assertEqual(asgi_span.data['http']['error'], 'Something went wrong.') - self.assertIsNone(asgi_span.data['http']['params']) - - def test_path_templates(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/foo/1') - - self.assertEqual(result.status_code, 200) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert asgi_span.ec == 1 + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/test_request_args" + assert asgi_span.data["http"]["path_tpl"] == "/test_request_args" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 500 + assert asgi_span.data["http"]["error"] == "Something went wrong." + assert not asgi_span.data["http"]["params"] + + def test_path_templates(self) -> None: + with tracer.start_as_current_span("test"): + result = requests.get(testenv["sanic_server"] + "/foo/1") + + assert result.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/foo/1') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/foo/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - def test_secret_scrubbing(self): - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/?secret=shhh') - - self.assertEqual(result.status_code, 200) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/foo/1" + assert asgi_span.data["http"]["path_tpl"] == "/foo/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + def test_secret_scrubbing(self) -> None: + with tracer.start_as_current_span("test"): + result = requests.get(testenv["sanic_server"] + "/?secret=shhh") + + assert result.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertEqual(asgi_span.data['http']['params'], 'secret=') - - def test_synthetic_request(self): - request_headers = { - 'X-INSTANA-SYNTHETIC': '1' - } - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/', headers=request_headers) - - self.assertEqual(result.status_code, 200) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert asgi_span.data["http"]["params"] == "secret=" + + def test_synthetic_request(self) -> None: + request_headers = {"X-INSTANA-SYNTHETIC": "1"} + with tracer.start_as_current_span("test"): + result = requests.get( + testenv["sanic_server"] + "/", headers=request_headers + ) + + assert result.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertIsNotNone(asgi_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) - - def test_request_header_capture(self): - request_headers = { - 'X-Capture-This': 'this', - 'X-Capture-That': 'that' - } - with tracer.start_active_span('test'): - result = requests.get(testenv["sanic_server"] + '/', headers=request_headers) - - self.assertEqual(result.status_code, 200) - - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) - - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert asgi_span.sy + assert not urllib3_span.sy + assert not test_span.sy + + def test_request_header_capture(self) -> None: + request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} + with tracer.start_as_current_span("test"): + result = requests.get( + testenv["sanic_server"] + "/", headers=request_headers + ) + + assert result.status_code == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data['http']['path'], '/') - self.assertEqual(asgi_span.data['http']['path_tpl'], '/') - self.assertEqual(asgi_span.data['http']['method'], 'GET') - self.assertEqual(asgi_span.data['http']['status'], 200) - self.assertIsNone(asgi_span.data['http']['error']) - self.assertIsNone(asgi_span.data['http']['params']) - - self.assertIn("X-Capture-This", asgi_span.data["http"]["header"]) - self.assertEqual("this", asgi_span.data["http"]["header"]["X-Capture-This"]) - self.assertIn("X-Capture-That", asgi_span.data["http"]["header"]) - self.assertEqual("that", asgi_span.data["http"]["header"]["X-Capture-That"]) - - def test_response_header_capture(self): - with tracer.start_active_span("test"): + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/" + assert asgi_span.data["http"]["path_tpl"] == "/" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This" in asgi_span.data["http"]["header"] + assert "this" == asgi_span.data["http"]["header"]["X-Capture-This"] + assert "X-Capture-That" in asgi_span.data["http"]["header"] + assert "that" == asgi_span.data["http"]["header"]["X-Capture-That"] + + def test_response_header_capture(self) -> None: + with tracer.start_as_current_span("test"): result = requests.get(testenv["sanic_server"] + "/response_headers") - self.assertEqual(result.status_code, 200) + assert result.status_code == 200 - spans = tracer.recorder.queued_spans() - self.assertEqual(len(spans), 3) + spans = self.recorder.queued_spans() + assert len(spans) == 3 - span_filter = lambda span: span.n == "sdk" and span.data['sdk']['name'] == 'test' + span_filter = ( + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) test_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(test_span) + assert test_span span_filter = lambda span: span.n == "urllib3" urllib3_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(urllib3_span) + assert urllib3_span - span_filter = lambda span: span.n == 'asgi' + span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) - self.assertIsNotNone(asgi_span) + assert asgi_span self.assertTraceContextPropagated(test_span, urllib3_span) self.assertTraceContextPropagated(urllib3_span, asgi_span) - self.assertIn("X-INSTANA-T", result.headers) - self.assertEqual(result.headers["X-INSTANA-T"], asgi_span.t) - self.assertIn("X-INSTANA-S", result.headers) - self.assertEqual(result.headers["X-INSTANA-S"], asgi_span.s) - self.assertIn("X-INSTANA-L", result.headers) - self.assertEqual(result.headers["X-INSTANA-L"], '1') - self.assertIn("Server-Timing", result.headers) - self.assertEqual(result.headers["Server-Timing"], ("intid;desc=%s" % asgi_span.t)) - - self.assertIsNone(asgi_span.ec) - self.assertEqual(asgi_span.data['http']['host'], '127.0.0.1:1337') - self.assertEqual(asgi_span.data["http"]["path"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["path_tpl"], "/response_headers") - self.assertEqual(asgi_span.data["http"]["method"], "GET") - self.assertEqual(asgi_span.data["http"]["status"], 200) - - self.assertIsNone(asgi_span.data["http"]["error"]) - self.assertIsNone(asgi_span.data["http"]["params"]) - - self.assertIn("X-Capture-This-Too", asgi_span.data["http"]["header"]) - self.assertEqual("this too", asgi_span.data["http"]["header"]["X-Capture-This-Too"]) - self.assertIn("X-Capture-That-Too", asgi_span.data["http"]["header"]) - self.assertEqual("that too", asgi_span.data["http"]["header"]["X-Capture-That-Too"]) + assert "X-INSTANA-T" in result.headers + assert result.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in result.headers + assert result.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" + assert asgi_span.data["http"]["path"] == "/response_headers" + assert asgi_span.data["http"]["path_tpl"] == "/response_headers" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This-Too" in asgi_span.data["http"]["header"] + assert "this too" == asgi_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in asgi_span.data["http"]["header"] + assert "that too" == asgi_span.data["http"]["header"]["X-Capture-That-Too"] diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 3d39e2d6..5cb4e0ad 100644 --- a/tests/requirements-310.txt +++ b/tests/requirements-310.txt @@ -35,7 +35,7 @@ pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -sanic==21.6.2 +sanic>=19.9.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 diff --git a/tests/requirements-312.txt b/tests/requirements-312.txt index cb7fe7c8..4b7afae7 100644 --- a/tests/requirements-312.txt +++ b/tests/requirements-312.txt @@ -33,7 +33,7 @@ pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -sanic==21.6.2 +sanic>=19.9.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index 6d532421..49905ab7 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -43,10 +43,9 @@ pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -# Newer versions of sanic are not supported -# And this old version is not installable on 3.13 because of the `httptools` dependency fails to compile: +# Sanic is not installable on 3.13 because `httptools, uvloop` dependencies fail to compile: # `too few arguments to function ‘_PyLong_AsByteArray’` -#sanic==21.6.2 +#sanic>=19.9.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 diff --git a/tests/requirements.txt b/tests/requirements.txt index 9e8c1165..96cdd823 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -34,7 +34,7 @@ pytz>=2024.1 redis>=3.5.3 requests-mock responses<=0.17.0 -sanic==21.6.2 +sanic>=19.9.0 sqlalchemy>=2.0.0 tornado>=4.5.3,<6.0 uvicorn>=0.13.4 diff --git a/tests/test_utils.py b/tests/test_utils.py index 57fc5cf6..2be9a228 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,10 +3,10 @@ class _TraceContextMixin: def assertTraceContextPropagated(self, parent_span, child_span): - self.assertEqual(parent_span.t, child_span.t) - self.assertEqual(parent_span.s, child_span.p) - self.assertNotEqual(parent_span.s, child_span.s) + assert parent_span.t == child_span.t + assert parent_span.s == child_span.p + assert parent_span.s != child_span.s def assertErrorLogging(self, spans): for span in spans: - self.assertIsNone(span.ec) + assert not span.ec From b3c35b3b65bff5845fcbcd4c2f8077a45417d678 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Mon, 16 Sep 2024 15:53:47 +0530 Subject: [PATCH 154/172] sanic: use the lightweight sanic_testing module (cherry picked from commit 6ff67a143cd0eecf45dceb9bc9b25c84a47f3c66) Signed-off-by: Varsha GS --- src/instana/instrumentation/sanic_inst.py | 17 +- src/instana/util/traceutils.py | 2 +- tests/apps/sanic_app/__init__.py | 30 -- tests/frameworks/test_sanic.py | 457 +++++++++++----------- tests/requirements-310.txt | 1 + tests/requirements-312.txt | 1 + tests/requirements-313.txt | 1 + tests/requirements.txt | 1 + 8 files changed, 250 insertions(+), 260 deletions(-) delete mode 100644 tests/apps/sanic_app/__init__.py diff --git a/src/instana/instrumentation/sanic_inst.py b/src/instana/instrumentation/sanic_inst.py index 6d50b4b5..acc3f621 100644 --- a/src/instana/instrumentation/sanic_inst.py +++ b/src/instana/instrumentation/sanic_inst.py @@ -1,4 +1,5 @@ -# (c) Copyright IBM Corp. 2024 +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 """ Instrumentation for Sanic @@ -21,12 +22,8 @@ from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format - if hasattr(sanic.request, "types"): - from sanic.request.types import Request - from sanic.response.types import HTTPResponse - else: - from sanic.request import Request - from sanic.response import HTTPResponse + from sanic.request import Request + from sanic.response import HTTPResponse @wrapt.patch_function_wrapper("sanic.app", "Sanic.__init__") def init_with_instana( @@ -54,7 +51,7 @@ def request_with_instana(request: Request) -> None: token = context.attach(ctx) request.ctx.token = token - span.set_attribute("span.kind", SpanKind.CLIENT) + span.set_attribute("span.kind", SpanKind.SERVER) span.set_attribute("http.path", request.path) span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) span.set_attribute(SpanAttributes.HTTP_HOST, request.host) @@ -81,7 +78,7 @@ def request_with_instana(request: Request) -> None: @app.exception(Exception) def exception_with_instana(request: Request, exception: Exception) -> None: try: - if not hasattr(request.ctx, "span"): + if not hasattr(request.ctx, "span"): # pragma: no cover return span = request.ctx.span @@ -98,7 +95,7 @@ def exception_with_instana(request: Request, exception: Exception) -> None: @app.middleware("response") def response_with_instana(request: Request, response: HTTPResponse) -> None: try: - if not hasattr(request.ctx, "span"): + if not hasattr(request.ctx, "span"): # pragma: no cover return span = request.ctx.span diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index edcba787..06b821ca 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -15,7 +15,7 @@ def extract_custom_headers(tracing_span, headers) -> None: # Headers are in the following format: b'x-header-1' for header_key, value in headers.items(): if header_key.lower() == custom_header.lower(): - tracing_span.set_attribute("http.header.%s" % custom_header, value) + tracing_span.set_attribute(f"http.header.{custom_header}", value) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) diff --git a/tests/apps/sanic_app/__init__.py b/tests/apps/sanic_app/__init__.py deleted file mode 100644 index f96b079b..00000000 --- a/tests/apps/sanic_app/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2021 - - -import uvicorn - -from tests.helpers import testenv - -testenv["sanic_port"] = 1337 -testenv["sanic_server"] = f"http://127.0.0.1:{testenv['sanic_port']}" - - -def launch_sanic(): - from .server import app - from instana.singletons import agent - - # Hack together a manual custom headers list; We'll use this in tests - agent.options.extra_http_headers = [ - "X-Capture-This", - "X-Capture-That", - "X-Capture-This-Too", - "X-Capture-That-Too", - ] - - uvicorn.run( - app, - host="127.0.0.1", - port=testenv["sanic_port"], - log_level="critical", - ) diff --git a/tests/frameworks/test_sanic.py b/tests/frameworks/test_sanic.py index 923e0e1d..275b3e4f 100644 --- a/tests/frameworks/test_sanic.py +++ b/tests/frameworks/test_sanic.py @@ -1,55 +1,65 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -import time -import requests -import multiprocessing import pytest from typing import Generator +from sanic_testing.testing import SanicTestClient -from instana.singletons import tracer -from tests.helpers import testenv +from instana.singletons import tracer, agent from tests.helpers import get_first_span_by_filter from tests.test_utils import _TraceContextMixin +from tests.apps.sanic_app.server import app class TestSanic(_TraceContextMixin): + @classmethod + def setup_class(cls) -> None: + cls.client = SanicTestClient(app, port=1337, host="127.0.0.1") + + # Hack together a manual custom headers list; We'll use this in tests + agent.options.extra_http_headers = [ + "X-Capture-This", + "X-Capture-That", + "X-Capture-This-Too", + "X-Capture-That-Too", + ] + @pytest.fixture(autouse=True) def _resource(self) -> Generator[None, None, None]: """Setup and Teardown""" + # setup # Clear all spans before a test run self.recorder = tracer.span_processor self.recorder.clear_spans() - from tests.apps.sanic_app import launch_sanic - - self.proc = multiprocessing.Process(target=launch_sanic, args=(), daemon=True) - self.proc.start() - time.sleep(2) - yield - # Kill server after tests - self.proc.kill() def test_vanilla_get(self) -> None: - result = requests.get(testenv["sanic_server"] + "/") - - assert result.status_code == 200 - assert "X-INSTANA-T" in result.headers - assert "X-INSTANA-S" in result.headers - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers + request, response = self.client.get("/") + + assert response.status_code == 200 + assert "X-INSTANA-T" in response.headers + assert "X-INSTANA-S" in response.headers + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers spans = self.recorder.queued_spans() assert len(spans) == 1 assert spans[0].n == "asgi" def test_basic_get(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/") - - assert result.status_code == 200 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/", headers=headers) + + assert response.status_code == 200 spans = self.recorder.queued_spans() - assert len(spans) == 3 + assert len(spans) == 2 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -57,25 +67,20 @@ def test_basic_get(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -87,13 +92,20 @@ def test_basic_get(self) -> None: assert not asgi_span.data["http"]["params"] def test_404(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/foo/not_an_int") - - assert result.status_code == 404 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/foo/not_an_int", headers=headers) + + assert response.status_code == 404 spans = self.recorder.queued_spans() - assert len(spans) == 3 + assert len(spans) == 2 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -101,25 +113,20 @@ def test_404(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -131,13 +138,20 @@ def test_404(self) -> None: assert not asgi_span.data["http"]["params"] def test_sanic_exception(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/wrong") - - assert result.status_code == 400 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/wrong", headers=headers) + + assert response.status_code == 400 spans = self.recorder.queued_spans() - assert len(spans) == 4 + assert len(spans) == 3 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -145,25 +159,20 @@ def test_sanic_exception(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -175,13 +184,20 @@ def test_sanic_exception(self) -> None: assert not asgi_span.data["http"]["params"] def test_500_instana_exception(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/instana_exception") - - assert result.status_code == 500 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/instana_exception", headers=headers) + + assert response.status_code == 500 spans = self.recorder.queued_spans() - assert len(spans) == 4 + assert len(spans) == 3 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -189,25 +205,20 @@ def test_500_instana_exception(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert asgi_span.ec == 1 assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -219,13 +230,20 @@ def test_500_instana_exception(self) -> None: assert not asgi_span.data["http"]["params"] def test_500(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/test_request_args") - - assert result.status_code == 500 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/test_request_args", headers=headers) + + assert response.status_code == 500 spans = self.recorder.queued_spans() - assert len(spans) == 4 + assert len(spans) == 3 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -233,25 +251,20 @@ def test_500(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert asgi_span.ec == 1 assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -263,13 +276,20 @@ def test_500(self) -> None: assert not asgi_span.data["http"]["params"] def test_path_templates(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/foo/1") - - assert result.status_code == 200 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/foo/1", headers=headers) + + assert response.status_code == 200 spans = self.recorder.queued_spans() - assert len(spans) == 3 + assert len(spans) == 2 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -277,25 +297,20 @@ def test_path_templates(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -307,13 +322,20 @@ def test_path_templates(self) -> None: assert not asgi_span.data["http"]["params"] def test_secret_scrubbing(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/?secret=shhh") - - assert result.status_code == 200 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/?secret=shhh", headers=headers) + + assert response.status_code == 200 spans = self.recorder.queued_spans() - assert len(spans) == 3 + assert len(spans) == 2 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -321,25 +343,20 @@ def test_secret_scrubbing(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -351,16 +368,21 @@ def test_secret_scrubbing(self) -> None: assert asgi_span.data["http"]["params"] == "secret=" def test_synthetic_request(self) -> None: - request_headers = {"X-INSTANA-SYNTHETIC": "1"} - with tracer.start_as_current_span("test"): - result = requests.get( - testenv["sanic_server"] + "/", headers=request_headers - ) - - assert result.status_code == 200 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-INSTANA-SYNTHETIC": "1", + } + request, response = self.client.get("/", headers=headers) + + assert response.status_code == 200 spans = self.recorder.queued_spans() - assert len(spans) == 3 + assert len(spans) == 2 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -368,25 +390,20 @@ def test_synthetic_request(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -398,20 +415,25 @@ def test_synthetic_request(self) -> None: assert not asgi_span.data["http"]["params"] assert asgi_span.sy - assert not urllib3_span.sy assert not test_span.sy def test_request_header_capture(self) -> None: - request_headers = {"X-Capture-This": "this", "X-Capture-That": "that"} - with tracer.start_as_current_span("test"): - result = requests.get( - testenv["sanic_server"] + "/", headers=request_headers - ) - - assert result.status_code == 200 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + "X-Capture-This": "this", + "X-Capture-That": "that", + } + request, response = self.client.get("/", headers=headers) + + assert response.status_code == 200 spans = self.recorder.queued_spans() - assert len(spans) == 3 + assert len(spans) == 2 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -419,25 +441,20 @@ def test_request_header_capture(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) + self.assertTraceContextPropagated(test_span, asgi_span) - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" @@ -454,13 +471,20 @@ def test_request_header_capture(self) -> None: assert "that" == asgi_span.data["http"]["header"]["X-Capture-That"] def test_response_header_capture(self) -> None: - with tracer.start_as_current_span("test"): - result = requests.get(testenv["sanic_server"] + "/response_headers") - - assert result.status_code == 200 + with tracer.start_as_current_span("test") as span: + # As SanicTestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the sanic server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": str(span_context.trace_id), + "X-INSTANA-S": str(span_context.span_id), + } + request, response = self.client.get("/response_headers", headers=headers) + + assert response.status_code == 200 spans = self.recorder.queued_spans() - assert len(spans) == 3 + assert len(spans) == 2 span_filter = ( lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" @@ -468,25 +492,20 @@ def test_response_header_capture(self) -> None: test_span = get_first_span_by_filter(spans, span_filter) assert test_span - span_filter = lambda span: span.n == "urllib3" - urllib3_span = get_first_span_by_filter(spans, span_filter) - assert urllib3_span - span_filter = lambda span: span.n == "asgi" asgi_span = get_first_span_by_filter(spans, span_filter) assert asgi_span - self.assertTraceContextPropagated(test_span, urllib3_span) - self.assertTraceContextPropagated(urllib3_span, asgi_span) - - assert "X-INSTANA-T" in result.headers - assert result.headers["X-INSTANA-T"] == str(asgi_span.t) - assert "X-INSTANA-S" in result.headers - assert result.headers["X-INSTANA-S"] == str(asgi_span.s) - assert "X-INSTANA-L" in result.headers - assert result.headers["X-INSTANA-L"] == "1" - assert "Server-Timing" in result.headers - assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + self.assertTraceContextPropagated(test_span, asgi_span) + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == str(asgi_span.t) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == str(asgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) assert not asgi_span.ec assert asgi_span.data["http"]["host"] == "127.0.0.1:1337" diff --git a/tests/requirements-310.txt b/tests/requirements-310.txt index 5cb4e0ad..88be77c3 100644 --- a/tests/requirements-310.txt +++ b/tests/requirements-310.txt @@ -36,6 +36,7 @@ redis>=3.5.3 requests-mock responses<=0.17.0 sanic>=19.9.0 +sanic-testing>=24.6.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 diff --git a/tests/requirements-312.txt b/tests/requirements-312.txt index 4b7afae7..e2fdf83d 100644 --- a/tests/requirements-312.txt +++ b/tests/requirements-312.txt @@ -34,6 +34,7 @@ redis>=3.5.3 requests-mock responses<=0.17.0 sanic>=19.9.0 +sanic-testing>=24.6.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 diff --git a/tests/requirements-313.txt b/tests/requirements-313.txt index 49905ab7..46e24cc6 100644 --- a/tests/requirements-313.txt +++ b/tests/requirements-313.txt @@ -46,6 +46,7 @@ responses<=0.17.0 # Sanic is not installable on 3.13 because `httptools, uvloop` dependencies fail to compile: # `too few arguments to function ‘_PyLong_AsByteArray’` #sanic>=19.9.0 +#sanic-testing>=24.6.0 sqlalchemy>=2.0.0 uvicorn>=0.13.4 diff --git a/tests/requirements.txt b/tests/requirements.txt index 96cdd823..01cdd36f 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -35,6 +35,7 @@ redis>=3.5.3 requests-mock responses<=0.17.0 sanic>=19.9.0 +sanic-testing>=24.6.0 sqlalchemy>=2.0.0 tornado>=4.5.3,<6.0 uvicorn>=0.13.4 From 1188af26648a516578cc1c07162aa53209da4234 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Fri, 13 Sep 2024 12:45:53 +0300 Subject: [PATCH 155/172] refactor(couchbase): added otel instrumentation of couchbase --- src/instana/__init__.py | 2 +- src/instana/instrumentation/couchbase_inst.py | 129 ++++++++++++------ 2 files changed, 92 insertions(+), 39 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index df4ce2c8..bf3b8556 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -166,7 +166,7 @@ def boot_agent(): asyncio, # noqa: F401 boto3_inst, # noqa: F401 # cassandra_inst, # noqa: F401 - # couchbase_inst, # noqa: F401 + couchbase_inst, # noqa: F401 fastapi_inst, # noqa: F401 flask, # noqa: F401 # gevent_inst, # noqa: F401 diff --git a/src/instana/instrumentation/couchbase_inst.py b/src/instana/instrumentation/couchbase_inst.py index f65c639a..cb97e042 100644 --- a/src/instana/instrumentation/couchbase_inst.py +++ b/src/instana/instrumentation/couchbase_inst.py @@ -6,18 +6,21 @@ https://docs.couchbase.com/python-sdk/2.5/start-using-sdk.html """ +from typing import Any, Callable, Dict, Tuple, Union + import wrapt -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import couchbase + from couchbase.bucket import Bucket - if not (hasattr(couchbase, '__version__') and couchbase.__version__[0] == '2' - and (couchbase.__version__[2] > '3' - or (couchbase.__version__[2] == '3' and couchbase.__version__[4] >= '4')) - ): + if not hasattr(couchbase, "__version__") and ( + couchbase.__version__ < "2.3.4" or couchbase.__version__ >= "3.0.0" + ): logger.debug("Instana supports 2.3.4 <= couchbase_versions < 3.0.0. Skipping.") raise ImportError @@ -25,71 +28,121 @@ # List of operations to instrument # incr, incr_multi, decr, decr_multi, retrieve_in are wrappers around operations above - operations = ['upsert', 'insert', 'replace', 'append', 'prepend', 'get', 'rget', - 'touch', 'lock', 'unlock', 'remove', 'counter', 'mutate_in', 'lookup_in', - 'stats', 'ping', 'diagnostics', 'observe', - - 'upsert_multi', 'insert_multi', 'replace_multi', 'append_multi', - 'prepend_multi', 'get_multi', 'touch_multi', 'lock_multi', 'unlock_multi', - 'observe_multi', 'endure_multi', 'remove_multi', 'counter_multi'] - - def capture_kvs(scope, instance, query_arg, op): + operations = [ + "upsert", + "insert", + "replace", + "append", + "prepend", + "get", + "rget", + "touch", + "lock", + "unlock", + "remove", + "counter", + "mutate_in", + "lookup_in", + "stats", + "ping", + "diagnostics", + "observe", + "upsert_multi", + "insert_multi", + "replace_multi", + "append_multi", + "prepend_multi", + "get_multi", + "touch_multi", + "lock_multi", + "unlock_multi", + "observe_multi", + "endure_multi", + "remove_multi", + "counter_multi", + ] + + def collect_attributes( + span: InstanaSpan, + instance: Bucket, + query_arg: Union[N1QLQuery, object], + op: str, + ) -> None: try: - scope.span.set_tag('couchbase.hostname', instance.server_nodes[0]) - scope.span.set_tag('couchbase.bucket', instance.bucket) - scope.span.set_tag('couchbase.type', op) + span.set_attribute("couchbase.hostname", instance.server_nodes[0]) + span.set_attribute("couchbase.bucket", instance.bucket) + span.set_attribute("couchbase.type", op) - if query_arg is not None: + if query_arg: query = None if type(query_arg) is N1QLQuery: query = query_arg.statement else: query = query_arg - scope.span.set_tag('couchbase.sql', query) - except: + span.set_attribute("couchbase.sql", query) + except Exception: # No fail on key capture - best effort pass - def make_wrapper(op): - def wrapper(wrapped, instance, args, kwargs): + def make_wrapper(op: str) -> Callable: + def wrapper( + wrapped: Callable[..., object], + instance: couchbase.bucket.Bucket, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return if tracing_is_off(): return wrapped(*args, **kwargs) - with tracer.start_active_span("couchbase", child_of=parent_span) as scope: - capture_kvs(scope, instance, None, op) + with tracer.start_as_current_span( + "couchbase", span_context=parent_context + ) as span: + collect_attributes(span, instance, None, op) try: return wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) - scope.span.set_tag('couchbase.error', repr(e)) - raise + except Exception as exc: + span.record_exception(exc) + span.set_attribute("couchbase.error", repr(exc)) + logger.debug("Instana couchbase @ wrapper", exc_info=True) + return wrapper - def query_with_instana(wrapped, instance, args, kwargs): + def query_with_instana( + wrapped: Callable[..., object], + instance: couchbase.bucket.Bucket, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return if tracing_is_off(): return wrapped(*args, **kwargs) - with tracer.start_active_span("couchbase", child_of=parent_span) as scope: - capture_kvs(scope, instance, args[0], 'n1ql_query') + with tracer.start_as_current_span( + "couchbase", span_context=parent_context + ) as span: try: + collect_attributes(span, instance, args[0], "n1ql_query") return wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) - scope.span.set_tag('couchbase.error', repr(e)) - raise + except Exception as exc: + span.record_exception(exc) + span.set_attribute("couchbase.error", repr(exc)) + logger.debug("Instana couchbase @ query_with_instana", exc_info=True) logger.debug("Instrumenting couchbase") - wrapt.wrap_function_wrapper('couchbase.bucket', 'Bucket.n1ql_query', query_with_instana) + wrapt.wrap_function_wrapper( + "couchbase.bucket", "Bucket.n1ql_query", query_with_instana + ) for op in operations: f = make_wrapper(op) - wrapt.wrap_function_wrapper('couchbase.bucket', 'Bucket.%s' % op, f) + wrapt.wrap_function_wrapper("couchbase.bucket", f"Bucket.{op}", f) except ImportError: pass From 8dee449441a5e449ba5136be5d7a2ded40545ef8 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Fri, 13 Sep 2024 12:46:05 +0300 Subject: [PATCH 156/172] unittests(couchbase): added unittests of couchbase instrumentation --- tests/clients/test_couchbase.py | 1598 +++++++++++++++++-------------- tests/conftest.py | 5 +- 2 files changed, 879 insertions(+), 724 deletions(-) diff --git a/tests/clients/test_couchbase.py b/tests/clients/test_couchbase.py index 61cdf6ef..9064fb06 100644 --- a/tests/clients/test_couchbase.py +++ b/tests/clients/test_couchbase.py @@ -3,175 +3,192 @@ import os import time -import unittest +from typing import Generator +from unittest.mock import patch + +import pytest from instana.singletons import agent, tracer -from ..helpers import testenv, get_first_span_by_name, get_first_span_by_filter +from tests.helpers import testenv, get_first_span_by_name, get_first_span_by_filter from couchbase.admin import Admin from couchbase.cluster import Cluster from couchbase.bucket import Bucket -from couchbase.exceptions import CouchbaseTransientError, HTTPError, KeyExistsError, NotFoundError +from couchbase.exceptions import ( + CouchbaseTransientError, + HTTPError, + KeyExistsError, + NotFoundError, +) import couchbase.subdocument as SD from couchbase.n1ql import N1QLQuery # Delete any pre-existing buckets. Create new. -cb_adm = Admin(testenv['couchdb_username'], testenv['couchdb_password'], host=testenv['couchdb_host'], port=8091) +cb_adm = Admin( + testenv["couchdb_username"], + testenv["couchdb_password"], + host=testenv["couchdb_host"], + port=8091, +) # Make sure a test bucket exists try: - cb_adm.bucket_create('travel-sample') - cb_adm.wait_ready('travel-sample', timeout=30) + cb_adm.bucket_create("travel-sample") + cb_adm.wait_ready("travel-sample", timeout=30) except HTTPError: pass -@unittest.skipIf(not os.environ.get("COUCHBASE_TEST"), reason="") -class TestStandardCouchDB(unittest.TestCase): - def setup_class(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder - self.cluster = Cluster('couchbase://%s' % testenv['couchdb_host']) - self.bucket = Bucket('couchbase://%s/travel-sample' % testenv['couchdb_host'], - username=testenv['couchdb_username'], password=testenv['couchdb_password']) - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ - agent.options.allow_exit_as_root = False - - def setup_method(self, _): - self.bucket.upsert('test-key', 1) +class TestStandardCouchDB: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor + self.cluster = Cluster("couchbase://%s" % testenv["couchdb_host"]) + self.bucket = Bucket( + "couchbase://%s/travel-sample" % testenv["couchdb_host"], + username=testenv["couchdb_username"], + password=testenv["couchdb_password"], + ) + self.bucket.upsert("test-key", 1) time.sleep(0.5) self.recorder.clear_spans() + yield + agent.options.allow_exit_as_root = False - def test_vanilla_get(self): + def test_vanilla_get(self) -> None: res = self.bucket.get("test-key") - self.assertTrue(res) - - def test_pipeline(self): - pass + assert res - def test_upsert(self): + def test_upsert(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.upsert("test_upsert", 1) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'upsert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "upsert" - def test_upsert_as_root_exit_span(self): + def test_upsert_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True res = self.bucket.upsert("test_upsert", 1) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span - self.assertEqual(cb_span.p, None) + assert not cb_span.p - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'upsert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "upsert" - def test_upsert_multi(self): + def test_upsert_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_upsert_multi'] = 1 - kvs['second_test_upsert_multi'] = 1 + kvs = {} + kvs["first_test_upsert_multi"] = 1 + kvs["second_test_upsert_multi"] = 1 - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.upsert_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_upsert_multi'].success) - self.assertTrue(res['second_test_upsert_multi'].success) + assert res + assert res["first_test_upsert_multi"].success + assert res["second_test_upsert_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'upsert_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "upsert_multi" - def test_insert_new(self): + def test_insert_new(self) -> None: res = None try: - self.bucket.remove('test_insert_new') + self.bucket.remove("test_insert_new") except NotFoundError: pass - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.insert("test_insert_new", 1) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'insert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "insert" - def test_insert_existing(self): + def test_insert_existing(self) -> None: res = None try: self.bucket.insert("test_insert", 1) @@ -179,113 +196,119 @@ def test_insert_existing(self): pass try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.insert("test_insert", 1) except KeyExistsError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("_KeyExistsError") - self.assertFalse(found == -1, "Error substring not found.") + assert not found == -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'insert') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "insert" - def test_insert_multi(self): + def test_insert_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_upsert_multi'] = 1 - kvs['second_test_upsert_multi'] = 1 + kvs = {} + kvs["first_test_upsert_multi"] = 1 + kvs["second_test_upsert_multi"] = 1 try: - self.bucket.remove('first_test_upsert_multi') - self.bucket.remove('second_test_upsert_multi') + self.bucket.remove("first_test_upsert_multi") + self.bucket.remove("second_test_upsert_multi") except NotFoundError: pass - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.insert_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_upsert_multi'].success) - self.assertTrue(res['second_test_upsert_multi'].success) + assert res + assert res["first_test_upsert_multi"].success + assert res["second_test_upsert_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'insert_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "insert_multi" - def test_replace(self): + def test_replace(self) -> None: res = None try: self.bucket.insert("test_replace", 1) except KeyExistsError: pass - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.replace("test_replace", 2) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'replace') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "replace" - def test_replace_non_existent(self): + def test_replace_non_existent(self) -> None: res = None try: @@ -294,969 +317,1102 @@ def test_replace_non_existent(self): pass try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.replace("test_replace", 2) except NotFoundError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("NotFoundError") - self.assertFalse(found == -1, "Error substring not found.") + assert not found == -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'replace') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "replace" - def test_replace_multi(self): + def test_replace_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_replace_multi'] = 1 - kvs['second_test_replace_multi'] = 1 + kvs = {} + kvs["first_test_replace_multi"] = 1 + kvs["second_test_replace_multi"] = 1 - self.bucket.upsert('first_test_replace_multi', "one") - self.bucket.upsert('second_test_replace_multi', "two") + self.bucket.upsert("first_test_replace_multi", "one") + self.bucket.upsert("second_test_replace_multi", "two") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.replace_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_replace_multi'].success) - self.assertTrue(res['second_test_replace_multi'].success) + assert res + assert res["first_test_replace_multi"].success + assert res["second_test_replace_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'replace_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "replace_multi" - def test_append(self): + def test_append(self) -> None: self.bucket.upsert("test_append", "one") res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.append("test_append", "two") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'append') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "append" - def test_append_multi(self): + def test_append_multi(self) -> None: res = None kvs = dict() - kvs['first_test_append_multi'] = "ok1" - kvs['second_test_append_multi'] = "ok2" + kvs["first_test_append_multi"] = "ok1" + kvs["second_test_append_multi"] = "ok2" - self.bucket.upsert('first_test_append_multi', "one") - self.bucket.upsert('second_test_append_multi', "two") + self.bucket.upsert("first_test_append_multi", "one") + self.bucket.upsert("second_test_append_multi", "two") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.append_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_append_multi'].success) - self.assertTrue(res['second_test_append_multi'].success) + assert res + assert res["first_test_append_multi"].success + assert res["second_test_append_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'append_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "append_multi" - def test_prepend(self): + def test_prepend(self) -> None: self.bucket.upsert("test_prepend", "one") res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.prepend("test_prepend", "two") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'prepend') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "prepend" - def test_prepend_multi(self): + def test_prepend_multi(self) -> None: res = None - kvs = dict() - kvs['first_test_prepend_multi'] = "ok1" - kvs['second_test_prepend_multi'] = "ok2" + kvs = {} + kvs["first_test_prepend_multi"] = "ok1" + kvs["second_test_prepend_multi"] = "ok2" - self.bucket.upsert('first_test_prepend_multi', "one") - self.bucket.upsert('second_test_prepend_multi', "two") + self.bucket.upsert("first_test_prepend_multi", "one") + self.bucket.upsert("second_test_prepend_multi", "two") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.prepend_multi(kvs) - self.assertTrue(res) - self.assertTrue(res['first_test_prepend_multi'].success) - self.assertTrue(res['second_test_prepend_multi'].success) + assert res + assert res["first_test_prepend_multi"].success + assert res["second_test_prepend_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'prepend_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "prepend_multi" - def test_get(self): + def test_get(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.get("test-key") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'get') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "get" - def test_rget(self): + def test_rget(self) -> None: res = None try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.rget("test-key", replica_index=None) except CouchbaseTransientError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("CouchbaseTransientError") - self.assertFalse(found == -1, "Error substring not found.") + assert found != -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'rget') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "rget" - def test_get_not_found(self): + def test_get_not_found(self) -> None: res = None try: - self.bucket.remove('test_get_not_found') + self.bucket.remove("test_get_not_found") except NotFoundError: pass try: - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.get("test_get_not_found") except NotFoundError: pass - self.assertIsNone(res) + assert not res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertEqual(cb_span.ec, 1) + assert cb_span.stack + assert cb_span.ec == 1 # Just search for the substring of the exception class found = cb_span.data["couchbase"]["error"].find("NotFoundError") - self.assertFalse(found == -1, "Error substring not found.") + assert found != -1 - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'get') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "get" - def test_get_multi(self): + def test_get_multi(self) -> None: res = None - self.bucket.upsert('first_test_get_multi', "one") - self.bucket.upsert('second_test_get_multi', "two") + self.bucket.upsert("first_test_get_multi", "one") + self.bucket.upsert("second_test_get_multi", "two") - with tracer.start_active_span('test'): - res = self.bucket.get_multi(['first_test_get_multi', 'second_test_get_multi']) + with tracer.start_as_current_span("test"): + res = self.bucket.get_multi( + ["first_test_get_multi", "second_test_get_multi"] + ) - self.assertTrue(res) - self.assertTrue(res['first_test_get_multi'].success) - self.assertTrue(res['second_test_get_multi'].success) + assert res + assert res["first_test_get_multi"].success + assert res["second_test_get_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'get_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "get_multi" - def test_touch(self): + def test_touch(self) -> None: res = None self.bucket.upsert("test_touch", 1) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.touch("test_touch") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'touch') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "touch" - def test_touch_multi(self): + def test_touch_multi(self) -> None: res = None - self.bucket.upsert('first_test_touch_multi', "one") - self.bucket.upsert('second_test_touch_multi', "two") + self.bucket.upsert("first_test_touch_multi", "one") + self.bucket.upsert("second_test_touch_multi", "two") - with tracer.start_active_span('test'): - res = self.bucket.touch_multi(['first_test_touch_multi', 'second_test_touch_multi']) + with tracer.start_as_current_span("test"): + res = self.bucket.touch_multi( + ["first_test_touch_multi", "second_test_touch_multi"] + ) - self.assertTrue(res) - self.assertTrue(res['first_test_touch_multi'].success) - self.assertTrue(res['second_test_touch_multi'].success) + assert res + assert res["first_test_touch_multi"].success + assert res["second_test_touch_multi"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'touch_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "touch_multi" - def test_lock(self): + def test_lock(self) -> None: res = None self.bucket.upsert("test_lock_unlock", "lock_this") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): rv = self.bucket.lock("test_lock_unlock", ttl=5) - self.assertTrue(rv) - self.assertTrue(rv.success) + assert rv + assert rv.success # upsert automatically unlocks the key res = self.bucket.upsert("test_lock_unlock", "updated", rv.cas) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 + + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" cb_lock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_lock_span) + assert cb_lock_span + + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "upsert" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "upsert" cb_upsert_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_upsert_span) + assert cb_upsert_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_lock_span.t) - self.assertEqual(test_span.t, cb_upsert_span.t) - - self.assertEqual(cb_lock_span.p, test_span.s) - self.assertEqual(cb_upsert_span.p, test_span.s) - - self.assertTrue(cb_lock_span.stack) - self.assertIsNone(cb_lock_span.ec) - self.assertTrue(cb_upsert_span.stack) - self.assertIsNone(cb_upsert_span.ec) - - self.assertEqual(cb_lock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_lock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_lock_span.data["couchbase"]["type"], 'lock') - self.assertEqual(cb_upsert_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_upsert_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_upsert_span.data["couchbase"]["type"], 'upsert') - - def test_lock_unlock(self): + assert cb_lock_span.t == test_span.t + assert cb_upsert_span.t == test_span.t + + assert cb_lock_span.p == test_span.s + assert cb_upsert_span.p == test_span.s + + assert cb_lock_span.stack + assert not cb_lock_span.ec + assert cb_upsert_span.stack + assert not cb_upsert_span.ec + + assert ( + cb_lock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_lock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_lock_span.data["couchbase"]["type"] == "lock" + assert ( + cb_upsert_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_upsert_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_upsert_span.data["couchbase"]["type"] == "upsert" + + def test_lock_unlock(self) -> None: res = None self.bucket.upsert("test_lock_unlock", "lock_this") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): rv = self.bucket.lock("test_lock_unlock", ttl=5) - self.assertTrue(rv) - self.assertTrue(rv.success) + assert rv + assert rv.success # upsert automatically unlocks the key res = self.bucket.unlock("test_lock_unlock", rv.cas) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" + + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock" cb_lock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_lock_span) + assert cb_lock_span + + def filter(span): + return span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock" - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock" cb_unlock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_unlock_span) + assert cb_unlock_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_lock_span.t) - self.assertEqual(test_span.t, cb_unlock_span.t) - - self.assertEqual(cb_lock_span.p, test_span.s) - self.assertEqual(cb_unlock_span.p, test_span.s) - - self.assertTrue(cb_lock_span.stack) - self.assertIsNone(cb_lock_span.ec) - self.assertTrue(cb_unlock_span.stack) - self.assertIsNone(cb_unlock_span.ec) - - self.assertEqual(cb_lock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_lock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_lock_span.data["couchbase"]["type"], 'lock') - self.assertEqual(cb_unlock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_unlock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_unlock_span.data["couchbase"]["type"], 'unlock') - - def test_lock_unlock_muilti(self): + assert cb_lock_span.t == test_span.t + assert cb_unlock_span.t == test_span.t + + assert cb_lock_span.p == test_span.s + assert cb_unlock_span.p == test_span.s + + assert cb_lock_span.stack + assert not cb_lock_span.ec + assert cb_unlock_span.stack + assert not cb_unlock_span.ec + + assert ( + cb_lock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_lock_span.data["couchbase"]["bucket"], "travel-sample" + assert cb_lock_span.data["couchbase"]["type"], "lock" + assert ( + cb_unlock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_unlock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_unlock_span.data["couchbase"]["type"] == "unlock" + + def test_lock_unlock_muilti(self) -> None: res = None self.bucket.upsert("test_lock_unlock_multi_1", "lock_this") self.bucket.upsert("test_lock_unlock_multi_2", "lock_this") keys_to_lock = ("test_lock_unlock_multi_1", "test_lock_unlock_multi_2") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): rv = self.bucket.lock_multi(keys_to_lock, ttl=5) - self.assertTrue(rv) - self.assertTrue(rv['test_lock_unlock_multi_1'].success) - self.assertTrue(rv['test_lock_unlock_multi_2'].success) + assert rv + assert rv["test_lock_unlock_multi_1"].success + assert rv["test_lock_unlock_multi_2"].success res = self.bucket.unlock_multi(rv) - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 + + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + def filter(span): + return ( + span.n == "couchbase" and span.data["couchbase"]["type"] == "lock_multi" + ) - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "lock_multi" cb_lock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_lock_span) + assert cb_lock_span + + def filter(span): + return ( + span.n == "couchbase" + and span.data["couchbase"]["type"] == "unlock_multi" + ) - filter = lambda span: span.n == "couchbase" and span.data["couchbase"]["type"] == "unlock_multi" cb_unlock_span = get_first_span_by_filter(spans, filter) - self.assertTrue(cb_unlock_span) + assert cb_unlock_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_lock_span.t) - self.assertEqual(test_span.t, cb_unlock_span.t) - - self.assertEqual(cb_lock_span.p, test_span.s) - self.assertEqual(cb_unlock_span.p, test_span.s) - - self.assertTrue(cb_lock_span.stack) - self.assertIsNone(cb_lock_span.ec) - self.assertTrue(cb_unlock_span.stack) - self.assertIsNone(cb_unlock_span.ec) - - self.assertEqual(cb_lock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_lock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_lock_span.data["couchbase"]["type"], 'lock_multi') - self.assertEqual(cb_unlock_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_unlock_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_unlock_span.data["couchbase"]["type"], 'unlock_multi') - - def test_remove(self): + assert cb_lock_span.t == test_span.t + assert cb_unlock_span.t == test_span.t + + assert cb_lock_span.p == test_span.s + assert cb_unlock_span.p == test_span.s + + assert cb_lock_span.stack + assert not cb_lock_span.ec + assert cb_unlock_span.stack + assert not cb_unlock_span.ec + + assert ( + cb_lock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_lock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_lock_span.data["couchbase"]["type"] == "lock_multi" + assert ( + cb_unlock_span.data["couchbase"]["hostname"] + == f"{testenv['couchdb_host']}:8091" + ) + assert cb_unlock_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_unlock_span.data["couchbase"]["type"] == "unlock_multi" + + def test_remove(self) -> None: res = None self.bucket.upsert("test_remove", 1) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.remove("test_remove") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'remove') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "remove" - def test_remove_multi(self): + def test_remove_multi(self) -> None: res = None self.bucket.upsert("test_remove_multi_1", 1) self.bucket.upsert("test_remove_multi_2", 1) keys_to_remove = ("test_remove_multi_1", "test_remove_multi_2") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.remove_multi(keys_to_remove) - self.assertTrue(res) - self.assertTrue(res['test_remove_multi_1'].success) - self.assertTrue(res['test_remove_multi_2'].success) + assert res + assert res["test_remove_multi_1"].success + assert res["test_remove_multi_2"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'remove_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "remove_multi" - def test_counter(self): + def test_counter(self) -> None: res = None self.bucket.upsert("test_counter", 1) - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.counter("test_counter", delta=10) - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'counter') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "counter" - def test_counter_multi(self): + def test_counter_multi(self) -> None: res = None self.bucket.upsert("first_test_counter", 1) self.bucket.upsert("second_test_counter", 1) - with tracer.start_active_span('test'): - res = self.bucket.counter_multi(("first_test_counter", "second_test_counter")) + with tracer.start_as_current_span("test"): + res = self.bucket.counter_multi( + ("first_test_counter", "second_test_counter") + ) - self.assertTrue(res) - self.assertTrue(res['first_test_counter'].success) - self.assertTrue(res['second_test_counter'].success) + assert res + assert res["first_test_counter"].success + assert res["second_test_counter"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'counter_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "counter_multi" - def test_mutate_in(self): + def test_mutate_in(self) -> None: res = None - self.bucket.upsert('king_arthur', {'name': 'Arthur', 'email': 'kingarthur@couchbase.com', - 'interests': ['Holy Grail', 'African Swallows']}) - - with tracer.start_active_span('test'): - res = self.bucket.mutate_in('king_arthur', - SD.array_addunique('interests', 'Cats'), - SD.counter('updates', 1)) - - self.assertTrue(res) - self.assertTrue(res.success) + self.bucket.upsert( + "king_arthur", + { + "name": "Arthur", + "email": "kingarthur@couchbase.com", + "interests": ["Holy Grail", "African Swallows"], + }, + ) + + with tracer.start_as_current_span("test"): + res = self.bucket.mutate_in( + "king_arthur", + SD.array_addunique("interests", "Cats"), + SD.counter("updates", 1), + ) + + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'mutate_in') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "mutate_in" - def test_lookup_in(self): + def test_lookup_in(self) -> None: res = None - self.bucket.upsert('king_arthur', {'name': 'Arthur', 'email': 'kingarthur@couchbase.com', - 'interests': ['Holy Grail', 'African Swallows']}) - - with tracer.start_active_span('test'): - res = self.bucket.lookup_in('king_arthur', - SD.get('email'), - SD.get('interests')) - - self.assertTrue(res) - self.assertTrue(res.success) + self.bucket.upsert( + "king_arthur", + { + "name": "Arthur", + "email": "kingarthur@couchbase.com", + "interests": ["Holy Grail", "African Swallows"], + }, + ) + + with tracer.start_as_current_span("test"): + res = self.bucket.lookup_in( + "king_arthur", SD.get("email"), SD.get("interests") + ) + + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'lookup_in') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "lookup_in" - def test_stats(self): + def test_stats(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.stats() - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'stats') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "stats" - def test_ping(self): + def test_ping(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.ping() - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'ping') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "ping" - def test_diagnostics(self): + def test_diagnostics(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.diagnostics() - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'diagnostics') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "diagnostics" - def test_observe(self): + def test_observe(self) -> None: res = None - self.bucket.upsert('test_observe', 1) + self.bucket.upsert("test_observe", 1) - with tracer.start_active_span('test'): - res = self.bucket.observe('test_observe') + with tracer.start_as_current_span("test"): + res = self.bucket.observe("test_observe") - self.assertTrue(res) - self.assertTrue(res.success) + assert res + assert res.success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'observe') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "observe" - def test_observe_multi(self): + def test_observe_multi(self) -> None: res = None - self.bucket.upsert('test_observe_multi_1', 1) - self.bucket.upsert('test_observe_multi_2', 1) + self.bucket.upsert("test_observe_multi_1", 1) + self.bucket.upsert("test_observe_multi_2", 1) - keys_to_observe = ('test_observe_multi_1', 'test_observe_multi_2') + keys_to_observe = ("test_observe_multi_1", "test_observe_multi_2") - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): res = self.bucket.observe_multi(keys_to_observe) - self.assertTrue(res) - self.assertTrue(res['test_observe_multi_1'].success) - self.assertTrue(res['test_observe_multi_2'].success) + assert res + assert res["test_observe_multi_1"].success + assert res["test_observe_multi_2"].success spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'observe_multi') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "observe_multi" - def test_raw_n1ql_query(self): + def test_query_with_instana_tracing_off(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.couchbase_inst.tracing_is_off", return_value=True + ): res = self.bucket.n1ql_query("SELECT 1") + assert res - self.assertTrue(res) + def test_query_with_instana_exception(self) -> None: + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.couchbase_inst.collect_attributes", + side_effect=Exception("test-error"), + ): + self.bucket.n1ql_query("SELECT 1") spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + cb_span = get_first_span_by_name(spans, "couchbase") - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + assert cb_span.data["couchbase"]["error"] == "Exception('test-error')" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + def test_raw_n1ql_query(self) -> None: + res = None + + with tracer.start_as_current_span("test"): + res = self.bucket.n1ql_query("SELECT 1") + + assert res + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" + + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) + assert cb_span.stack + assert not cb_span.ec - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'n1ql_query') - self.assertEqual(cb_span.data["couchbase"]["sql"], 'SELECT 1') + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "n1ql_query" + assert cb_span.data["couchbase"]["sql"] == "SELECT 1" - def test_n1ql_query(self): + def test_n1ql_query(self) -> None: res = None - with tracer.start_active_span('test'): - res = self.bucket.n1ql_query(N1QLQuery('SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"')) + with tracer.start_as_current_span("test"): + res = self.bucket.n1ql_query( + N1QLQuery( + 'SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"' + ) + ) - self.assertTrue(res) + assert res spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertTrue(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cb_span = get_first_span_by_name(spans, 'couchbase') - self.assertTrue(cb_span) + cb_span = get_first_span_by_name(spans, "couchbase") + assert cb_span # Same traceId and parent relationship - self.assertEqual(test_span.t, cb_span.t) - self.assertEqual(cb_span.p, test_span.s) - - self.assertTrue(cb_span.stack) - self.assertIsNone(cb_span.ec) - - self.assertEqual(cb_span.data["couchbase"]["hostname"], "%s:8091" % testenv['couchdb_host']) - self.assertEqual(cb_span.data["couchbase"]["bucket"], 'travel-sample') - self.assertEqual(cb_span.data["couchbase"]["type"], 'n1ql_query') - self.assertEqual(cb_span.data["couchbase"]["sql"], 'SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"') + assert cb_span.t == test_span.t + assert cb_span.p == test_span.s + + assert cb_span.stack + assert not cb_span.ec + + assert ( + cb_span.data["couchbase"]["hostname"] == f"{testenv['couchdb_host']}:8091" + ) + assert cb_span.data["couchbase"]["bucket"] == "travel-sample" + assert cb_span.data["couchbase"]["type"] == "n1ql_query" + assert ( + cb_span.data["couchbase"]["sql"] + == 'SELECT name FROM `travel-sample` WHERE brewery_id ="mishawaka_brewing"' + ) diff --git a/tests/conftest.py b/tests/conftest.py index 94fac26c..0f4e5e66 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,7 +38,6 @@ # TODO: remove the following entries as the migration of the instrumentation # codes are finalised. collect_ignore_glob.append("*clients/test_cassandra*") -collect_ignore_glob.append("*clients/test_couchbase*") collect_ignore_glob.append("*clients/test_google*") collect_ignore_glob.append("*clients/test_pika*") collect_ignore_glob.append("*clients/test_redis*") @@ -55,8 +54,8 @@ # if not os.environ.get("CASSANDRA_TEST"): # collect_ignore_glob.append("*test_cassandra*") -# if not os.environ.get("COUCHBASE_TEST"): -# collect_ignore_glob.append("*test_couchbase*") +if not os.environ.get("COUCHBASE_TEST"): + collect_ignore_glob.append("*test_couchbase*") # if not os.environ.get("GEVENT_STARLETTE_TEST"): # collect_ignore_glob.append("*test_gevent*") From 54340c1d332b59b5c7e7957ba66440471d9fcdbd Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 12 Sep 2024 18:53:47 +0300 Subject: [PATCH 157/172] refactor(cassandra): added cassandra otel instrumentation --- src/instana/__init__.py | 1 + src/instana/instrumentation/cassandra_inst.py | 125 +++++++++++------- 2 files changed, 76 insertions(+), 50 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index bf3b8556..6384a2af 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -188,6 +188,7 @@ def boot_agent(): client, # noqa: F401 server, # noqa: F401 ) + # from instana.instrumentation.aws import lambda_inst # noqa: F401 # from instana.instrumentation.celery import hooks # noqa: F401 from instana.instrumentation.django import middleware # noqa: F401 diff --git a/src/instana/instrumentation/cassandra_inst.py b/src/instana/instrumentation/cassandra_inst.py index da828d18..3b6e9713 100644 --- a/src/instana/instrumentation/cassandra_inst.py +++ b/src/instana/instrumentation/cassandra_inst.py @@ -6,78 +6,103 @@ https://docs.datastax.com/en/developer/python-driver/3.20/ https://github.com/datastax/python-driver """ + +from typing import Any, Callable, Dict, Tuple import wrapt -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import cassandra - - consistency_levels = dict({0: "ANY", - 1: "ONE", - 2: "TWO", - 3: "THREE", - 4: "QUORUM", - 5: "ALL", - 6: "LOCAL_QUORUM", - 7: "EACH_QUORUM", - 8: "SERIAL", - 9: "LOCAL_SERIAL", - 10: "LOCAL_ONE"}) - - - def collect_response(span, fn): - tried_hosts = list() + from cassandra.cluster import ResponseFuture, Session + + consistency_levels = dict( + { + 0: "ANY", + 1: "ONE", + 2: "TWO", + 3: "THREE", + 4: "QUORUM", + 5: "ALL", + 6: "LOCAL_QUORUM", + 7: "EACH_QUORUM", + 8: "SERIAL", + 9: "LOCAL_SERIAL", + 10: "LOCAL_ONE", + } + ) + + def collect_attributes( + span: InstanaSpan, + fn: ResponseFuture, + ) -> None: + tried_hosts = [] for host in fn.attempted_hosts: - tried_hosts.append("%s:%d" % (host.endpoint.address, host.endpoint.port)) + tried_hosts.append(f"{host.endpoint.address}:{host.endpoint.port}") - span.set_tag("cassandra.triedHosts", tried_hosts) - span.set_tag("cassandra.coordHost", fn.coordinator_host) + span.set_attribute("cassandra.triedHosts", tried_hosts) + span.set_attribute("cassandra.coordHost", fn.coordinator_host) cl = fn.query.consistency_level if cl and cl in consistency_levels: - span.set_tag("cassandra.achievedConsistency", consistency_levels[cl]) - - - def cb_request_finish(results, span, fn): - collect_response(span, fn) - span.finish() - - - def cb_request_error(results, span, fn): - collect_response(span, fn) + span.set_attribute("cassandra.achievedConsistency", consistency_levels[cl]) + + def cb_request_finish( + _, + span: InstanaSpan, + fn: ResponseFuture, + ) -> None: + collect_attributes(span, fn) + span.end() + + def cb_request_error( + results: Dict[str, Any], + span: InstanaSpan, + fn: ResponseFuture, + ) -> None: + collect_attributes(span, fn) span.mark_as_errored({"cassandra.error": results.summary}) - span.finish() + span.end() - - def request_init_with_instana(fn): + def request_init_with_instana( + fn: ResponseFuture, + ) -> None: tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None if tracing_is_off(): return - ctags = {} + attributes = {} if isinstance(fn.query, cassandra.query.SimpleStatement): - ctags["cassandra.query"] = fn.query.query_string + attributes["cassandra.query"] = fn.query.query_string elif isinstance(fn.query, cassandra.query.BoundStatement): - ctags["cassandra.query"] = fn.query.prepared_statement.query_string - - ctags["cassandra.keyspace"] = fn.session.keyspace - ctags["cassandra.cluster"] = fn.session.cluster.metadata.cluster_name - - with tracer.start_active_span("cassandra", child_of=parent_span, - tags=ctags, finish_on_close=False) as scope: - fn.add_callback(cb_request_finish, scope.span, fn) - fn.add_errback(cb_request_error, scope.span, fn) - - - @wrapt.patch_function_wrapper('cassandra.cluster', 'Session.__init__') - def init_with_instana(wrapped, instance, args, kwargs): + attributes["cassandra.query"] = fn.query.prepared_statement.query_string + + attributes["cassandra.keyspace"] = fn.session.keyspace + attributes["cassandra.cluster"] = fn.session.cluster.metadata.cluster_name + + with tracer.start_as_current_span( + "cassandra", + span_context=parent_context, + attributes=attributes, + end_on_exit=False, + ) as span: + fn.add_callback(cb_request_finish, span, fn) + fn.add_errback(cb_request_error, span, fn) + + @wrapt.patch_function_wrapper("cassandra.cluster", "Session.__init__") + def init_with_instana( + wrapped: Callable[..., object], + instance: Session, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: session = wrapped(*args, **kwargs) instance.add_request_init_listener(request_init_with_instana) return session - logger.debug("Instrumenting cassandra") except ImportError: From fd43a53bf786fb0bf2f0568a95d25465fd5a0f41 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Thu, 12 Sep 2024 18:54:01 +0300 Subject: [PATCH 158/172] unittest(cassandra): added unittests of cassandra otel instrumentation --- tests/clients/test_cassandra-driver.py | 319 +++++++++++++------------ tests/conftest.py | 5 +- 2 files changed, 163 insertions(+), 161 deletions(-) diff --git a/tests/clients/test_cassandra-driver.py b/tests/clients/test_cassandra-driver.py index 44f05a21..3493de14 100644 --- a/tests/clients/test_cassandra-driver.py +++ b/tests/clients/test_cassandra-driver.py @@ -1,268 +1,271 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import os -import time import random -import unittest - -from instana.singletons import agent, tracer -from ..helpers import testenv, get_first_span_by_name +import time +from typing import Generator -from cassandra.cluster import Cluster +import pytest from cassandra import ConsistencyLevel +from cassandra.cluster import Cluster from cassandra.query import SimpleStatement -cluster = Cluster([testenv['cassandra_host']], load_balancing_policy=None) +from instana.singletons import agent, tracer +from tests.helpers import get_first_span_by_name, testenv + +cluster = Cluster([testenv["cassandra_host"]], load_balancing_policy=None) session = cluster.connect() session.execute( - "CREATE KEYSPACE IF NOT EXISTS instana_tests WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};") -session.set_keyspace('instana_tests') -session.execute("CREATE TABLE IF NOT EXISTS users(" - "id int PRIMARY KEY," - "name text," - "age text," - "email varint," - "phone varint" - ");") - - -@unittest.skipUnless(os.environ.get("CASSANDRA_TEST"), reason="") -class TestCassandra(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder + "CREATE KEYSPACE IF NOT EXISTS instana_tests WITH replication = {'class':'SimpleStrategy', 'replication_factor':1};" +) +session.set_keyspace("instana_tests") +session.execute( + "CREATE TABLE IF NOT EXISTS users(" + "id int PRIMARY KEY," + "name text," + "age text," + "email varint," + "phone varint" + ");" +) + + +class TestCassandra: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor self.recorder.clear_spans() - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + yield agent.options.allow_exit_as_root = False - def test_untraced_execute(self): - res = session.execute('SELECT name, age, email FROM users') + def test_untraced_execute(self) -> None: + res = session.execute("SELECT name, age, email FROM users") - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 - def test_untraced_execute_error(self): + def test_untraced_execute_error(self) -> None: res = None try: - res = session.execute('Not a valid query') - except: + res = session.execute("Not a valid query") + except Exception: pass - self.assertIsNone(res) + assert not res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(0, len(spans)) + assert len(spans) == 0 - def test_execute(self): + def test_execute(self) -> None: res = None - with tracer.start_active_span('test'): - res = session.execute('SELECT name, age, email FROM users') + with tracer.start_as_current_span("test"): + res = session.execute("SELECT name, age, email FROM users") - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_execute_as_root_exit_span(self): + def test_execute_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True - res = session.execute('SELECT name, age, email FROM users') + res = session.execute("SELECT name, age, email FROM users") - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan - self.assertIsNone(cspan.p) + assert not cspan.p - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_execute_async(self): + def test_execute_async(self) -> None: res = None - with tracer.start_active_span('test'): - res = session.execute_async('SELECT name, age, email FROM users').result() + with tracer.start_as_current_span("test"): + res = session.execute_async("SELECT name, age, email FROM users").result() - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_simple_statement(self): + def test_simple_statement(self) -> None: res = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): query = SimpleStatement( - 'SELECT name, age, email FROM users', - is_idempotent=True + "SELECT name, age, email FROM users", is_idempotent=True ) res = session.execute(query) - self.assertIsNotNone(res) + assert res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) + assert cspan.stack + assert not cspan.ec - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'SELECT name, age, email FROM users') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "SELECT name, age, email FROM users" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] - def test_execute_error(self): + def test_execute_error(self) -> None: res = None try: - with tracer.start_active_span('test'): - res = session.execute('Not a real query') - except: + with tracer.start_as_current_span("test"): + res = session.execute("Not a real query") + except Exception: pass - self.assertIsNone(res) + assert not res time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) + assert cspan.t == test_span.t + assert cspan.p == test_span.s - self.assertIsNotNone(cspan.stack) - self.assertEqual(cspan.ec, 1) + assert cspan.stack + assert cspan.ec == 1 - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'Not a real query') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertIsNone(cspan.data["cassandra"]["achievedConsistency"]) - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertEqual(cspan.data["cassandra"]["error"], "Syntax error in CQL query") + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert cspan.data["cassandra"]["query"] == "Not a real query" + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert not cspan.data["cassandra"]["achievedConsistency"] + assert cspan.data["cassandra"]["triedHosts"] + assert cspan.data["cassandra"]["error"] == "Syntax error in CQL query" - def test_prepared_statement(self): + def test_prepared_statement(self) -> None: prepared = None - result = None - with tracer.start_active_span('test'): - prepared = session.prepare('INSERT INTO users (id, name, age) VALUES (?, ?, ?)') + with tracer.start_as_current_span("test"): + prepared = session.prepare( + "INSERT INTO users (id, name, age) VALUES (?, ?, ?)" + ) prepared.consistency_level = ConsistencyLevel.QUORUM - result = session.execute(prepared, (random.randint(0, 1000000), "joe", "17")) + session.execute(prepared, (random.randint(0, 1000000), "joe", "17")) - self.assertIsNotNone(prepared) - self.assertIsNotNone(result) + assert prepared time.sleep(0.5) spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - test_span = get_first_span_by_name(spans, 'sdk') - self.assertIsNotNone(test_span) - self.assertEqual(test_span.data["sdk"]["name"], 'test') + test_span = get_first_span_by_name(spans, "sdk") + assert test_span + assert test_span.data["sdk"]["name"] == "test" - cspan = get_first_span_by_name(spans, 'cassandra') - self.assertIsNotNone(cspan) + cspan = get_first_span_by_name(spans, "cassandra") + assert cspan # Same traceId and parent relationship - self.assertEqual(test_span.t, cspan.t) - self.assertEqual(cspan.p, test_span.s) - - self.assertIsNotNone(cspan.stack) - self.assertIsNone(cspan.ec) - - self.assertEqual(cspan.data["cassandra"]["cluster"], 'Test Cluster') - self.assertEqual(cspan.data["cassandra"]["query"], 'INSERT INTO users (id, name, age) VALUES (?, ?, ?)') - self.assertEqual(cspan.data["cassandra"]["keyspace"], 'instana_tests') - self.assertEqual(cspan.data["cassandra"]["achievedConsistency"], "QUORUM") - self.assertIsNotNone(cspan.data["cassandra"]["triedHosts"]) - self.assertIsNone(cspan.data["cassandra"]["error"]) + assert test_span.t == cspan.t + assert cspan.p == test_span.s + + assert cspan.stack + assert not cspan.ec + + assert cspan.data["cassandra"]["cluster"] == "Test Cluster" + assert ( + cspan.data["cassandra"]["query"] + == "INSERT INTO users (id, name, age) VALUES (?, ?, ?)" + ) + assert cspan.data["cassandra"]["keyspace"] == "instana_tests" + assert cspan.data["cassandra"]["achievedConsistency"] == "QUORUM" + assert cspan.data["cassandra"]["triedHosts"] + assert not cspan.data["cassandra"]["error"] diff --git a/tests/conftest.py b/tests/conftest.py index 0f4e5e66..425ba008 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,7 +37,6 @@ # TODO: remove the following entries as the migration of the instrumentation # codes are finalised. -collect_ignore_glob.append("*clients/test_cassandra*") collect_ignore_glob.append("*clients/test_google*") collect_ignore_glob.append("*clients/test_pika*") collect_ignore_glob.append("*clients/test_redis*") @@ -51,8 +50,8 @@ # # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will # # be run explicitly. (So always exclude them here) -# if not os.environ.get("CASSANDRA_TEST"): -# collect_ignore_glob.append("*test_cassandra*") +if not os.environ.get("CASSANDRA_TEST"): + collect_ignore_glob.append("*test_cassandra*") if not os.environ.get("COUCHBASE_TEST"): collect_ignore_glob.append("*test_couchbase*") From f6bf1b17b12f21b4f95099f639caa3070c028d20 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 18 Sep 2024 13:27:14 +0300 Subject: [PATCH 159/172] refactor(redis): added instrumentation of redis --- src/instana/__init__.py | 2 +- src/instana/instrumentation/redis.py | 103 ++++++++++++++++----------- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 6384a2af..7f870b6a 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -178,7 +178,7 @@ def boot_agent(): psycopg2, # noqa: F401 pymongo, # noqa: F401 pymysql, # noqa: F401 - # redis, # noqa: F401 + redis, # noqa: F401 # sqlalchemy, # noqa: F401 starlette_inst, # noqa: F401 sanic_inst, # noqa: F401 diff --git a/src/instana/instrumentation/redis.py b/src/instana/instrumentation/redis.py index 5c9ed522..ec439ef4 100644 --- a/src/instana/instrumentation/redis.py +++ b/src/instana/instrumentation/redis.py @@ -2,94 +2,115 @@ # (c) Copyright Instana Inc. 2018 +from typing import Any, Callable, Dict, Tuple import wrapt -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off - +from instana.log import logger +from instana.span.span import InstanaSpan +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: import redis EXCLUDED_PARENT_SPANS = ["redis", "celery-client", "celery-worker"] - def collect_tags(span, instance, args, kwargs): + def collect_attributes( + span: InstanaSpan, + instance: redis.client.Redis, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> None: try: ckw = instance.connection_pool.connection_kwargs - span.set_tag("driver", "redis-py") + span.set_attribute("driver", "redis-py") - host = ckw.get('host', None) - port = ckw.get('port', '6379') - db = ckw.get('db', None) + host = ckw.get("host", None) + port = ckw.get("port", "6379") + db = ckw.get("db", None) - if host is not None: - url = "redis://%s:%s" % (host, port) + if host: + url = f"redis://{host}:{port}" if db is not None: - url = url + "/%s" % db - span.set_tag('connection', url) - - except: - logger.debug("redis.collect_tags non-fatal error", exc_info=True) - - return span - - - def execute_command_with_instana(wrapped, instance, args, kwargs): + url = f"{url}/{db}" + span.set_attribute("connection", url) + except Exception: + logger.debug("redis.collect_attributes non-fatal error", exc_info=True) + + def execute_command_with_instana( + wrapped: Callable[..., object], + instance: redis.client.Redis, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, operation_name = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return - if (tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS)): + if tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS): return wrapped(*args, **kwargs) - with tracer.start_active_span("redis", child_of=parent_span) as scope: + with tracer.start_as_current_span("redis", span_context=parent_context) as span: try: - collect_tags(scope.span, instance, args, kwargs) - if (len(args) > 0): - scope.span.set_tag("command", args[0]) + collect_attributes(span, instance, args, kwargs) + if len(args) > 0: + span.set_attribute("command", args[0]) rv = wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) + except Exception as exc: + span.record_exception(exc) raise else: return rv - - def execute_with_instana(wrapped, instance, args, kwargs): + def execute_with_instana( + wrapped: Callable[..., object], + instance: redis.client.Redis, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: tracer, parent_span, operation_name = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None # If we're not tracing, just return - if (tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS)): + if tracing_is_off() or (operation_name in EXCLUDED_PARENT_SPANS): return wrapped(*args, **kwargs) - with tracer.start_active_span("redis", child_of=parent_span) as scope: + with tracer.start_as_current_span("redis", span_context=parent_context) as span: try: - collect_tags(scope.span, instance, args, kwargs) - scope.span.set_tag("command", 'PIPELINE') + collect_attributes(span, instance, args, kwargs) + span.set_attribute("command", "PIPELINE") pipe_cmds = [] for e in instance.command_stack: pipe_cmds.append(e[0][0]) - scope.span.set_tag("subCommands", pipe_cmds) + span.set_attribute("subCommands", pipe_cmds) except Exception as e: # If anything breaks during K/V collection, just log a debug message logger.debug("Error collecting pipeline commands", exc_info=True) try: rv = wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) + except Exception as exc: + span.record_exception(exc) raise else: return rv - if redis.VERSION < (3,0,0): - wrapt.wrap_function_wrapper('redis.client', 'BasePipeline.execute', execute_with_instana) - wrapt.wrap_function_wrapper('redis.client', 'StrictRedis.execute_command', execute_command_with_instana) + if redis.VERSION < (3, 0, 0): + wrapt.wrap_function_wrapper( + "redis.client", "BasePipeline.execute", execute_with_instana + ) + wrapt.wrap_function_wrapper( + "redis.client", "StrictRedis.execute_command", execute_command_with_instana + ) else: - wrapt.wrap_function_wrapper('redis.client', 'Pipeline.execute', execute_with_instana) - wrapt.wrap_function_wrapper('redis.client', 'Redis.execute_command', execute_command_with_instana) + wrapt.wrap_function_wrapper( + "redis.client", "Pipeline.execute", execute_with_instana + ) + wrapt.wrap_function_wrapper( + "redis.client", "Redis.execute_command", execute_command_with_instana + ) logger.debug("Instrumenting redis") except ImportError: From d087f7a33d5d48c1ff0117623057d23706c9a544 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 18 Sep 2024 13:27:28 +0300 Subject: [PATCH 160/172] unittest(redis): added unittests for redis --- tests/clients/test_redis.py | 596 ++++++++++++++++++++---------------- tests/conftest.py | 1 - tests/helpers.py | 1 + 3 files changed, 339 insertions(+), 259 deletions(-) diff --git a/tests/clients/test_redis.py b/tests/clients/test_redis.py index e01a139b..4fa93e5c 100644 --- a/tests/clients/test_redis.py +++ b/tests/clients/test_redis.py @@ -1,376 +1,456 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest +import logging +from typing import Generator +from unittest.mock import patch +import pytest import redis -from redis.sentinel import Sentinel -from ..helpers import testenv +from instana.span.span import get_current_span +from tests.helpers import testenv from instana.singletons import agent, tracer -class TestRedis(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder +class TestRedis: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor self.recorder.clear_spans() - - # self.sentinel = Sentinel([(testenv['redis_host'], 26379)], socket_timeout=0.1) - # self.sentinel_master = self.sentinel.discover_master('mymaster') - # self.client = redis.Redis(host=self.sentinel_master[0]) - - self.client = redis.Redis(host=testenv['redis_host']) - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + self.client = redis.Redis(host=testenv["redis_host"], db=testenv["redis_db"]) + yield agent.options.allow_exit_as_root = False - def test_vanilla(self): - self.client.set('instrument', 'piano') - result = self.client.get('instrument') - - def test_set_get(self): + def test_set_get(self) -> None: result = None - with tracer.start_active_span('test'): - self.client.set('foox', 'barX') - self.client.set('fooy', 'barY') - result = self.client.get('foox') + with tracer.start_as_current_span("test"): + self.client.set("foox", "barX") + self.client.set("fooy", "barY") + result = self.client.get("foox") spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 - self.assertEqual(b'barX', result) + assert result == b"barX" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] test_span = spans[3] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) - self.assertEqual(test_span.t, rs2_span.t) - self.assertEqual(test_span.t, rs3_span.t) + assert rs1_span.t == test_span.t + assert rs2_span.t == test_span.t + assert rs3_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) - self.assertEqual(rs2_span.p, test_span.s) - self.assertEqual(rs3_span.p, test_span.s) + assert rs1_span.p == test_span.s + assert rs2_span.p == test_span.s + assert rs3_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not test_span.ec + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) - - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("SET", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) - - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data + + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs2_span.data["redis"]["command"] == "SET" + assert not rs2_span.data["redis"]["error"] + + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) - - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_set_get_as_root_span(self): + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_set_get_as_root_span(self) -> None: agent.options.allow_exit_as_root = True - self.client.set('foox', 'barX') - self.client.set('fooy', 'barY') - result = self.client.get('foox') + self.client.set("foox", "barX") + self.client.set("fooy", "barY") + result = self.client.get("foox") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 - self.assertEqual(b'barX', result) + assert result == b"barX" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Parent relationships - self.assertEqual(rs1_span.p, None) - self.assertEqual(rs2_span.p, None) - self.assertEqual(rs3_span.p, None) + assert not rs1_span.p + assert not rs2_span.p + assert not rs3_span.p # Error logging - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) - - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("SET", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) - - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data + + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs2_span.data["redis"]["command"] == "SET" + assert not rs2_span.data["redis"]["error"] + + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) - - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_set_incr_get(self): + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_set_incr_get(self) -> None: result = None - with tracer.start_active_span('test'): - self.client.set('counter', '10') - self.client.incr('counter') - result = self.client.get('counter') + with tracer.start_as_current_span("test"): + self.client.set("counter", "10") + self.client.incr("counter") + result = self.client.get("counter") spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 - self.assertEqual(b'11', result) + assert result == b"11" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] test_span = spans[3] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) - self.assertEqual(test_span.t, rs2_span.t) - self.assertEqual(test_span.t, rs3_span.t) + assert rs1_span.t == test_span.t + assert rs2_span.t == test_span.t + assert rs3_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) - self.assertEqual(rs2_span.p, test_span.s) - self.assertEqual(rs3_span.p, test_span.s) + assert rs1_span.p == test_span.s + assert rs2_span.p == test_span.s + assert rs3_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not test_span.ec + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) - - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("INCRBY", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) - - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data + + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs2_span.data["redis"]["command"] == "INCRBY" + assert not rs2_span.data["redis"]["error"] + + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) - - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_old_redis_client(self): + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_old_redis_client(self) -> None: result = None - with tracer.start_active_span('test'): - self.client.set('foox', 'barX') - self.client.set('fooy', 'barY') - result = self.client.get('foox') + with tracer.start_as_current_span("test"): + self.client.set("foox", "barX") + self.client.set("fooy", "barY") + result = self.client.get("foox") spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) + assert len(spans) == 4 - self.assertEqual(b'barX', result) + assert result == b"barX" rs1_span = spans[0] rs2_span = spans[1] rs3_span = spans[2] test_span = spans[3] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) - self.assertEqual(test_span.t, rs2_span.t) - self.assertEqual(test_span.t, rs3_span.t) + assert rs1_span.t == test_span.t + assert rs2_span.t == test_span.t + assert rs3_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) - self.assertEqual(rs2_span.p, test_span.s) - self.assertEqual(rs3_span.p, test_span.s) + assert rs1_span.p == test_span.s + assert rs2_span.p == test_span.s + assert rs3_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) - self.assertIsNone(rs2_span.ec) - self.assertIsNone(rs3_span.ec) + assert not test_span.ec + assert not rs1_span.ec + assert not rs2_span.ec + assert not rs3_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("SET", rs1_span.data["redis"]["command"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "SET" + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 # Redis span 2 - self.assertEqual('redis', rs2_span.n) - self.assertFalse('custom' in rs2_span.data) - self.assertTrue('redis' in rs2_span.data) + assert rs2_span.n == "redis" + assert "custom" not in rs2_span.data + assert "redis" in rs2_span.data - self.assertEqual('redis-py', rs2_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs2_span.data["redis"]["connection"]) - self.assertEqual("SET", rs2_span.data["redis"]["command"]) - self.assertIsNone(rs2_span.data["redis"]["error"]) + assert rs2_span.data["redis"]["driver"] == "redis-py" + assert ( + rs2_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) - self.assertIsNotNone(rs2_span.stack) - self.assertTrue(type(rs2_span.stack) is list) - self.assertGreater(len(rs2_span.stack), 0) + assert rs2_span.data["redis"]["command"] == "SET" + assert not rs2_span.data["redis"]["error"] - # Redis span 3 - self.assertEqual('redis', rs3_span.n) - self.assertFalse('custom' in rs3_span.data) - self.assertTrue('redis' in rs3_span.data) - - self.assertEqual('redis-py', rs3_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs3_span.data["redis"]["connection"]) - self.assertEqual("GET", rs3_span.data["redis"]["command"]) - self.assertIsNone(rs3_span.data["redis"]["error"]) + assert rs2_span.stack + assert isinstance(rs2_span.stack, list) + assert len(rs2_span.stack) > 0 - self.assertIsNotNone(rs3_span.stack) - self.assertTrue(type(rs3_span.stack) is list) - self.assertGreater(len(rs3_span.stack), 0) - - def test_pipelined_requests(self): + # Redis span 3 + assert rs3_span.n == "redis" + assert "custom" not in rs3_span.data + assert "redis" in rs3_span.data + + assert rs3_span.data["redis"]["driver"] == "redis-py" + assert ( + rs3_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs3_span.data["redis"]["command"] == "GET" + assert not rs3_span.data["redis"]["error"] + + assert rs3_span.stack + assert isinstance(rs3_span.stack, list) + assert len(rs3_span.stack) > 0 + + def test_pipelined_requests(self) -> None: result = None - with tracer.start_active_span('test'): + with tracer.start_as_current_span("test"): pipe = self.client.pipeline() - pipe.set('foox', 'barX') - pipe.set('fooy', 'barY') - pipe.get('foox') + pipe.set("foox", "barX") + pipe.set("fooy", "barY") + pipe.get("foox") result = pipe.execute() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 - self.assertEqual([True, True, b'barX'], result) + assert result == [True, True, b"barX"] rs1_span = spans[0] test_span = spans[1] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, rs1_span.t) + assert rs1_span.t == test_span.t # Parent relationships - self.assertEqual(rs1_span.p, test_span.s) + assert rs1_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rs1_span.ec) + assert not test_span.ec + assert not rs1_span.ec # Redis span 1 - self.assertEqual('redis', rs1_span.n) - self.assertFalse('custom' in rs1_span.data) - self.assertTrue('redis' in rs1_span.data) - - self.assertEqual('redis-py', rs1_span.data["redis"]["driver"]) - self.assertEqual("redis://%s:6379/0" % testenv['redis_host'], rs1_span.data["redis"]["connection"]) - self.assertEqual("PIPELINE", rs1_span.data["redis"]["command"]) - self.assertEqual(['SET', 'SET', 'GET'], rs1_span.data["redis"]["subCommands"]) - self.assertIsNone(rs1_span.data["redis"]["error"]) - - self.assertIsNotNone(rs1_span.stack) - self.assertTrue(type(rs1_span.stack) is list) - self.assertGreater(len(rs1_span.stack), 0) + assert rs1_span.n == "redis" + assert "custom" not in rs1_span.data + assert "redis" in rs1_span.data + + assert rs1_span.data["redis"]["driver"] == "redis-py" + assert ( + rs1_span.data["redis"]["connection"] + == f"redis://{testenv['redis_host']}:6379/0" + ) + assert rs1_span.data["redis"]["command"] == "PIPELINE" + assert rs1_span.data["redis"]["subCommands"] == ["SET", "SET", "GET"] + assert not rs1_span.data["redis"]["error"] + + assert rs1_span.stack + assert isinstance(rs1_span.stack, list) + assert len(rs1_span.stack) > 0 + + @patch( + "instana.instrumentation.redis.collect_attributes", + side_effect=Exception("test-error"), + ) + @patch("instana.span.span.InstanaSpan.record_exception") + def test_execute_command_with_instana_exception(self, mock_record_func, _) -> None: + with tracer.start_as_current_span("test"), pytest.raises( + Exception, match="test-error" + ): + self.client.set("counter", "10") + mock_record_func.assert_called() + + def test_execute_comand_with_instana_tracing_off(self) -> None: + with tracer.start_as_current_span("redis"): + response = self.client.set("counter", "10") + assert response + + def test_execute_with_instana_tracing_off(self) -> None: + result = None + with tracer.start_as_current_span("redis"): + pipe = self.client.pipeline() + pipe.set("foox", "barX") + pipe.set("fooy", "barY") + pipe.get("foox") + result = pipe.execute() + assert result == [True, True, b"barX"] + + def test_execute_with_instana_exception( + self, caplog: pytest.LogCaptureFixture + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + with tracer.start_as_current_span("test"), patch( + "instana.instrumentation.redis.collect_attributes", + side_effect=Exception("test-error"), + ): + pipe = self.client.pipeline() + pipe.set("foox", "barX") + pipe.set("fooy", "barY") + pipe.get("foox") + pipe.execute() + assert "Error collecting pipeline commands" in caplog.messages diff --git a/tests/conftest.py b/tests/conftest.py index 425ba008..660f1245 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,7 +39,6 @@ # codes are finalised. collect_ignore_glob.append("*clients/test_google*") collect_ignore_glob.append("*clients/test_pika*") -collect_ignore_glob.append("*clients/test_redis*") collect_ignore_glob.append("*clients/test_sql*") collect_ignore_glob.append("*frameworks/test_celery*") diff --git a/tests/helpers.py b/tests/helpers.py index 30caf5ac..7a24bdc8 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -46,6 +46,7 @@ Redis Environment """ testenv["redis_host"] = os.environ.get("REDIS_HOST", "127.0.0.1") +testenv["redis_db"] = os.environ.get("REDIS_DB", 0) """ MongoDB Environment From f64b24e5fa97ba42a8ed0cd1579a885a38fc1c30 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 10 Sep 2024 07:06:04 +0200 Subject: [PATCH 161/172] refactor: Pika instrumentation. Signed-off-by: Paulo Vital --- src/instana/__init__.py | 2 +- src/instana/instrumentation/pika.py | 329 ++++++++++++++++++---------- 2 files changed, 218 insertions(+), 113 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 7f870b6a..647b6253 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -173,7 +173,7 @@ def boot_agent(): # grpcio, # noqa: F401 logging, # noqa: F401 mysqlclient, # noqa: F401 - # pika, # noqa: F401 + pika, # noqa: F401 pep0249, # noqa: F401 psycopg2, # noqa: F401 pymongo, # noqa: F401 diff --git a/src/instana/instrumentation/pika.py b/src/instana/instrumentation/pika.py index cc9478cb..c8c74a5d 100644 --- a/src/instana/instrumentation/pika.py +++ b/src/instana/instrumentation/pika.py @@ -2,167 +2,261 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 - - -import wrapt -import opentracing -import types - -from ..log import logger -from ..singletons import tracer -from ..util.traceutils import get_tracer_tuple, tracing_is_off - try: - import pika - - - def _extract_broker_tags(span, conn): - span.set_tag("address", "%s:%d" % (conn.params.host, conn.params.port)) - - - def _extract_publisher_tags(span, conn, exchange, routing_key): - _extract_broker_tags(span, conn) - - span.set_tag("sort", "publish") - span.set_tag("key", routing_key) - span.set_tag("exchange", exchange) - - - def _extract_consumer_tags(span, conn, queue): - _extract_broker_tags(span, conn) + import types + from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterator, + Optional, + Tuple, + Union, + ) - span.set_tag("sort", "consume") - span.set_tag("queue", queue) - - - @wrapt.patch_function_wrapper('pika.channel', 'Channel.basic_publish') - def basic_publish_with_instana(wrapped, instance, args, kwargs): - def _bind_args(exchange, routing_key, body, properties=None, *args, **kwargs): + import pika + import wrapt + + from instana.log import logger + from instana.propagators.format import Format + from instana.singletons import tracer + from instana.util.traceutils import get_tracer_tuple, tracing_is_off + + if TYPE_CHECKING: + import pika.adapters.blocking_connection + import pika.channel + import pika.connection + + from instana.span.span import InstanaSpan + + def _extract_broker_attributes( + span: "InstanaSpan", conn: pika.connection.Connection + ) -> None: + span.set_attribute("address", f"{conn.params.host}:{conn.params.port}") + + def _extract_publisher_attributes( + span: "InstanaSpan", + conn: pika.connection.Connection, + exchange: str, + routing_key: str, + ) -> None: + _extract_broker_attributes(span, conn) + + span.set_attribute("sort", "publish") + span.set_attribute("key", routing_key) + span.set_attribute("exchange", exchange) + + def _extract_consumer_tags( + span: "InstanaSpan", conn: pika.connection.Connection, queue: str + ) -> None: + _extract_broker_attributes(span, conn) + + span.set_attribute("sort", "consume") + span.set_attribute("queue", queue) + + @wrapt.patch_function_wrapper("pika.channel", "Channel.basic_publish") + def basic_publish_with_instana( + wrapped: Callable[..., pika.channel.Channel.basic_publish], + instance: pika.channel.Channel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args( + exchange: str, + routing_key: str, + body: str, + properties: Optional[object] = None, + *args: object, + **kwargs: object, + ) -> Tuple[object, ...]: return (exchange, routing_key, body, properties, args, kwargs) - tracer, parent_span, _ = get_tracer_tuple() - + # If we're not tracing, just return if tracing_is_off(): return wrapped(*args, **kwargs) - (exchange, routing_key, body, properties, args, kwargs) = (_bind_args(*args, **kwargs)) + tracer, parent_span, _ = get_tracer_tuple() + parent_context = parent_span.get_span_context() if parent_span else None + + (exchange, routing_key, body, properties, args, kwargs) = _bind_args( + *args, **kwargs + ) - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_publisher_tags(scope.span, - conn=instance.connection, - routing_key=routing_key, - exchange=exchange) - except: - logger.debug("publish_with_instana: ", exc_info=True) + _extract_publisher_attributes( + span, + conn=instance.connection, + routing_key=routing_key, + exchange=exchange, + ) + except Exception: + logger.debug("pika publish_with_instana error: ", exc_info=True) # context propagation properties = properties or pika.BasicProperties() properties.headers = properties.headers or {} - tracer.inject(scope.span.context, opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) + tracer.inject( + span.context, + Format.HTTP_HEADERS, + properties.headers, + disable_w3c_trace_context=True, + ) args = (exchange, routing_key, body, properties) + args try: rv = wrapped(*args, **kwargs) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) else: return rv - - def basic_get_with_instana(wrapped, instance, args, kwargs): - def _bind_args(*args, **kwargs): + def basic_get_with_instana( + wrapped: Callable[ + ..., + Union[pika.channel.Channel.basic_get, pika.channel.Channel.basic_consume], + ], + instance: pika.channel.Channel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args(*args: object, **kwargs: object) -> Tuple[object, ...]: args = list(args) - queue = kwargs.pop('queue', None) or args.pop(0) - callback = kwargs.pop('callback', None) or kwargs.pop('on_message_callback', None) or args.pop(0) + queue = kwargs.pop("queue", None) or args.pop(0) + callback = ( + kwargs.pop("callback", None) + or kwargs.pop("on_message_callback", None) + or args.pop(0) + ) return (queue, callback, tuple(args), kwargs) queue, callback, args, kwargs = _bind_args(*args, **kwargs) - def _cb_wrapper(channel, method, properties, body): - parent_span = tracer.extract(opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) - - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + def _cb_wrapper( + channel: pika.channel.Channel, + method: pika.spec.Basic, + properties: pika.BasicProperties, + body: str, + ) -> None: + parent_context = tracer.extract( + Format.HTTP_HEADERS, properties.headers, disable_w3c_trace_context=True + ) + + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_consumer_tags(scope.span, - conn=instance.connection, - queue=queue) - except: - logger.debug("basic_get_with_instana: ", exc_info=True) + _extract_consumer_tags(span, conn=instance.connection, queue=queue) + except Exception: + logger.debug("pika basic_get_with_instana error: ", exc_info=True) try: callback(channel, method, properties, body) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) args = (queue, _cb_wrapper) + args return wrapped(*args, **kwargs) - @wrapt.patch_function_wrapper('pika.adapters.blocking_connection', 'BlockingChannel.basic_consume') - def basic_consume_with_instana(wrapped, instance, args, kwargs): - def _bind_args(queue, on_message_callback, *args, **kwargs): + @wrapt.patch_function_wrapper( + "pika.adapters.blocking_connection", "BlockingChannel.basic_consume" + ) + def basic_consume_with_instana( + wrapped: Callable[ + ..., pika.adapters.blocking_connection.BlockingChannel.basic_consume + ], + instance: pika.adapters.blocking_connection.BlockingChannel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args( + queue: str, + on_message_callback: object, + *args: object, + **kwargs: object, + ) -> Tuple[object, ...]: return (queue, on_message_callback, args, kwargs) queue, on_message_callback, args, kwargs = _bind_args(*args, **kwargs) - def _cb_wrapper(channel, method, properties, body): - parent_span = tracer.extract(opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) - - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + def _cb_wrapper( + channel: pika.channel.Channel, + method: pika.spec.Basic, + properties: pika.BasicProperties, + body: str, + ) -> None: + parent_context = tracer.extract( + Format.HTTP_HEADERS, properties.headers, disable_w3c_trace_context=True + ) + + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_consumer_tags(scope.span, - conn=instance.connection._impl, - queue=queue) - except: - logger.debug("basic_consume_with_instana: ", exc_info=True) + _extract_consumer_tags( + span, conn=instance.connection._impl, queue=queue + ) + except Exception: + logger.debug( + "pika basic_consume_with_instana error:", exc_info=True + ) try: on_message_callback(channel, method, properties, body) - except Exception as e: - scope.span.log_exception(e) - raise + except Exception as exc: + span.record_exception(exc) args = (queue, _cb_wrapper) + args return wrapped(*args, **kwargs) - - @wrapt.patch_function_wrapper('pika.adapters.blocking_connection', 'BlockingChannel.consume') - def consume_with_instana(wrapped, instance, args, kwargs): - def _bind_args(queue, *args, **kwargs): + @wrapt.patch_function_wrapper( + "pika.adapters.blocking_connection", "BlockingChannel.consume" + ) + def consume_with_instana( + wrapped: Callable[..., pika.adapters.blocking_connection.BlockingChannel], + instance: pika.adapters.blocking_connection.BlockingChannel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: + def _bind_args( + queue: str, *args: object, **kwargs: object + ) -> Tuple[object, ...]: return (queue, args, kwargs) - (queue, args, kwargs) = (_bind_args(*args, **kwargs)) + (queue, args, kwargs) = _bind_args(*args, **kwargs) - def _consume(gen): - for yilded in gen: + def _consume(gen: Iterator[object]) -> object: + for yielded in gen: # Bypass the delivery created due to inactivity timeout - if yilded is None or not any(yilded): - yield yilded + if not yielded or not any(yielded): + yield yielded continue - (method_frame, properties, body) = yilded + (method_frame, properties, body) = yielded - parent_span = tracer.extract(opentracing.Format.HTTP_HEADERS, properties.headers, - disable_w3c_trace_context=True) - with tracer.start_active_span("rabbitmq", child_of=parent_span) as scope: + parent_context = tracer.extract( + Format.HTTP_HEADERS, + properties.headers, + disable_w3c_trace_context=True, + ) + with tracer.start_as_current_span( + "rabbitmq", span_context=parent_context + ) as span: try: - _extract_consumer_tags(scope.span, - conn=instance.connection._impl, - queue=queue) - except: + _extract_consumer_tags( + span, conn=instance.connection._impl, queue=queue + ) + except Exception: logger.debug("consume_with_instana: ", exc_info=True) try: - yield yilded - except Exception as e: - scope.span.log_exception(e) - raise + yield yielded + except Exception as exc: + span.record_exception(exc) args = (queue,) + args res = wrapped(*args, **kwargs) @@ -172,20 +266,31 @@ def _consume(gen): else: return res - - @wrapt.patch_function_wrapper('pika.adapters.blocking_connection', 'BlockingChannel.__init__') - def _BlockingChannel___init__(wrapped, instance, args, kwargs): + @wrapt.patch_function_wrapper( + "pika.adapters.blocking_connection", "BlockingChannel.__init__" + ) + def _BlockingChannel___init__( + wrapped: Callable[ + ..., pika.adapters.blocking_connection.BlockingChannel.__init__ + ], + instance: pika.adapters.blocking_connection.BlockingChannel, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ) -> object: ret = wrapped(*args, **kwargs) - impl = getattr(instance, '_impl', None) + impl = getattr(instance, "_impl", None) - if impl and hasattr(impl.basic_consume, '__wrapped__'): + if impl and hasattr(impl.basic_consume, "__wrapped__"): impl.basic_consume = impl.basic_consume.__wrapped__ return ret - - wrapt.wrap_function_wrapper('pika.channel', 'Channel.basic_get', basic_get_with_instana) - wrapt.wrap_function_wrapper('pika.channel', 'Channel.basic_consume', basic_get_with_instana) + wrapt.wrap_function_wrapper( + "pika.channel", "Channel.basic_get", basic_get_with_instana + ) + wrapt.wrap_function_wrapper( + "pika.channel", "Channel.basic_consume", basic_get_with_instana + ) logger.debug("Instrumenting pika") except ImportError: From b55e3fe07b31520d9ee13d78cba8db7e403aa2ab Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Tue, 10 Sep 2024 07:08:14 +0200 Subject: [PATCH 162/172] tests(pika): adapt tests to OTel usage. Signed-off-by: Paulo Vital --- tests/clients/test_pika.py | 772 +++++++++++++++++++++---------------- tests/conftest.py | 1 - 2 files changed, 445 insertions(+), 328 deletions(-) diff --git a/tests/clients/test_pika.py b/tests/clients/test_pika.py index 887f4bf4..093c36cd 100644 --- a/tests/clients/test_pika.py +++ b/tests/clients/test_pika.py @@ -1,328 +1,177 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -import unittest import threading import time +from typing import Generator, Optional -import pika import mock +import pika +import pika.adapters.blocking_connection +import pika.channel +import pika.spec +import pytest from instana.singletons import agent, tracer -class _TestPika(unittest.TestCase): +class _TestPika: @staticmethod - @mock.patch('pika.connection.Connection') - def _create_connection(connection_class_mock=None): + @mock.patch("pika.connection.Connection") + def _create_connection(connection_class_mock=None) -> object: return connection_class_mock() - def _create_obj(self): + def _create_obj(self) -> NotImplementedError: raise NotImplementedError() - def setUp(self): - self.recorder = tracer.recorder + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """SetUp and TearDown""" + # setup + # Clear all spans before a test run + self.recorder = tracer.span_processor self.recorder.clear_spans() self.connection = self._create_connection() self._on_openok_callback = mock.Mock() self.obj = self._create_obj() - - def tearDown(self): + yield + # teardown del self.connection del self._on_openok_callback del self.obj + # Ensure that allow_exit_as_root has the default value agent.options.allow_exit_as_root = False -class TestPikaChannel(_TestPika): - def _create_obj(self): - return pika.channel.Channel(self.connection, 1, self._on_openok_callback) - - @mock.patch('pika.spec.Basic.Publish') - @mock.patch('pika.channel.Channel._send_method') - def test_basic_publish(self, send_method, _unused): - self.obj._set_state(self.obj.OPEN) - - with tracer.start_active_span("testing"): - self.obj.basic_publish("test.exchange", "test.queue", "Hello!") - - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - - rabbitmq_span = spans[0] - test_span = spans[1] - - self.assertIsNone(tracer.active_span) - - # Same traceId - self.assertEqual(test_span.t, rabbitmq_span.t) - - # Parent relationships - self.assertEqual(rabbitmq_span.p, test_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(rabbitmq_span.ec) - - # Span tags - self.assertEqual("test.exchange", rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual('publish', rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["key"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) - - send_method.assert_called_once_with( - pika.spec.Basic.Publish( - exchange="test.exchange", - routing_key="test.queue"), (pika.spec.BasicProperties(headers={ - "X-INSTANA-T": rabbitmq_span.t, - "X-INSTANA-S": rabbitmq_span.s, - "X-INSTANA-L": "1" - }), b"Hello!")) - - @mock.patch('pika.spec.Basic.Publish') - @mock.patch('pika.channel.Channel._send_method') - def test_basic_publish_as_root_exit_span(self, send_method, _unused): - agent.options.allow_exit_as_root = True - self.obj._set_state(self.obj.OPEN) - self.obj.basic_publish("test.exchange", "test.queue", "Hello!") - - spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) +class TestPikaBlockingChannel(_TestPika): + @mock.patch("pika.channel.Channel", spec=pika.channel.Channel) + def _create_obj( + self, channel_impl: mock.MagicMock + ) -> pika.adapters.blocking_connection.BlockingChannel: + self.impl = channel_impl() + self.impl.channel_number = 1 - # Parent relationships - self.assertIsNone(rabbitmq_span.p, None) + return pika.adapters.blocking_connection.BlockingChannel( + self.impl, self.connection + ) - # Error logging - self.assertIsNone(rabbitmq_span.ec) + def _generate_delivery( + self, consumer_tag: str, properties: pika.BasicProperties, body: str + ) -> None: + from pika.adapters.blocking_connection import _ConsumerDeliveryEvt - # Span tags - self.assertEqual("test.exchange", rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual('publish', rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["key"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) + # Wait until queue consumer is initialized + while self.obj._queue_consumer_generator is None: + time.sleep(0.25) - send_method.assert_called_once_with( - pika.spec.Basic.Publish( - exchange="test.exchange", - routing_key="test.queue"), (pika.spec.BasicProperties(headers={ - "X-INSTANA-T": rabbitmq_span.t, - "X-INSTANA-S": rabbitmq_span.s, - "X-INSTANA-L": "1" - }), b"Hello!")) - - @mock.patch('pika.spec.Basic.Publish') - @mock.patch('pika.channel.Channel._send_method') - def test_basic_publish_with_headers(self, send_method, _unused): - self.obj._set_state(self.obj.OPEN) + method = pika.spec.Basic.Deliver(consumer_tag=consumer_tag) + self.obj._on_consumer_generator_event( + _ConsumerDeliveryEvt(method, properties, body) + ) - with tracer.start_active_span("testing"): - self.obj.basic_publish("test.exchange", - "test.queue", - "Hello!", - pika.BasicProperties(headers={ - "X-Custom-1": "test" - })) + def test_consume(self) -> None: + consumed_deliveries = [] - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + def __consume() -> None: + for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): + # Skip deliveries generated due to inactivity + if delivery is not None and any(delivery): + consumed_deliveries.append(delivery) - rabbitmq_span = spans[0] - test_span = spans[1] + break - send_method.assert_called_once_with( - pika.spec.Basic.Publish( - exchange="test.exchange", - routing_key="test.queue"), (pika.spec.BasicProperties(headers={ - "X-Custom-1": "test", - "X-INSTANA-T": rabbitmq_span.t, - "X-INSTANA-S": rabbitmq_span.s, - "X-INSTANA-L": "1" - }), b"Hello!")) - - @mock.patch('pika.spec.Basic.Get') - def test_basic_get(self, _unused): - self.obj._set_state(self.obj.OPEN) + consumer_tag = "test.consumer" - body = "Hello!" - properties = pika.BasicProperties() + self.impl.basic_consume.return_value = consumer_tag + self.impl._generate_consumer_tag.return_value = consumer_tag + self.impl._consumers = {} - method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) - header_frame = pika.frame.Header(1, len(body), properties) + t = threading.Thread(target=__consume) + t.start() - cb = mock.Mock() + self._generate_delivery(consumer_tag, pika.BasicProperties(), "Hello!") - self.obj.basic_get("test.queue", cb) - self.obj._on_getok(method_frame, header_frame, body) + t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s # Error logging - self.assertIsNone(rabbitmq_span.ec) + assert not rabbitmq_span.ec # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) - - cb.assert_called_once_with(self.obj, pika.spec.Basic.GetOk, properties, body) - - @mock.patch('pika.spec.Basic.Get') - def test_basic_get_with_trace_context(self, _unused): - self.obj._set_state(self.obj.OPEN) - - body = "Hello!" - properties = pika.BasicProperties(headers={ - "X-INSTANA-T": "0000000000000001", - "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }) - - method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) - header_frame = pika.frame.Header(1, len(body), properties) - - cb = mock.Mock() - - self.obj.basic_get("test.queue", cb) - self.obj._on_getok(method_frame, header_frame, body) - - spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) - - # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) - - # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) - - @mock.patch('pika.spec.Basic.Consume') - def test_basic_consume(self, _unused): - self.obj._set_state(self.obj.OPEN) + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 - body = "Hello!" - properties = pika.BasicProperties() - - method_frame = pika.frame.Method(1, pika.spec.Basic.Deliver(consumer_tag="test")) - header_frame = pika.frame.Header(1, len(body), properties) - - cb = mock.Mock() - - self.obj.basic_consume("test.queue", cb, consumer_tag="test") - self.obj._on_deliver(method_frame, header_frame, body) - - spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] + assert len(consumed_deliveries) == 1 - self.assertIsNone(tracer.active_span) - - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) + def test_consume_with_trace_context(self) -> None: + consumed_deliveries = [] - # Error logging - self.assertIsNone(rabbitmq_span.ec) + def __consume(): + for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): + # Skip deliveries generated due to inactivity + if delivery is not None and any(delivery): + consumed_deliveries.append(delivery) + break - # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) + consumer_tag = "test.consumer" - cb.assert_called_once_with(self.obj, method_frame.method, properties, body) + self.impl.basic_consume.return_value = consumer_tag + self.impl._generate_consumer_tag.return_value = consumer_tag + self.impl._consumers = {} - @mock.patch('pika.spec.Basic.Consume') - def test_basic_consume_with_trace_context(self, _unused): - self.obj._set_state(self.obj.OPEN) + t = threading.Thread(target=__consume) + t.start() - body = "Hello!" - properties = pika.BasicProperties(headers={ + instana_headers = { "X-INSTANA-T": "0000000000000001", "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }) - - method_frame = pika.frame.Method(1, pika.spec.Basic.Deliver(consumer_tag="test")) - header_frame = pika.frame.Header(1, len(body), properties) + "X-INSTANA-L": "1", + } + self._generate_delivery( + consumer_tag, + pika.BasicProperties(headers=instana_headers), + "Hello!", + ) - cb = mock.Mock() - - self.obj.basic_consume(queue="test.queue", on_message_callback=cb, consumer_tag="test") - self.obj._on_deliver(method_frame, header_frame, body) + t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) - # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s + def test_consume_with_not_GeneratorType(self, mocker) -> None: + mocker.patch( + "instana.instrumentation.pika.isinstance", + return_value=False, + ) -class TestPikaBlockingChannel(_TestPika): - @mock.patch('pika.channel.Channel', spec=pika.channel.Channel) - def _create_obj(self, channel_impl): - self.impl = channel_impl() - self.impl.channel_number = 1 - - return pika.adapters.blocking_connection.BlockingChannel(self.impl, self.connection) - - def _generate_delivery(self, consumer_tag, properties, body): - from pika.adapters.blocking_connection import _ConsumerDeliveryEvt - - # Wait until queue consumer is initialized - while self.obj._queue_consumer_generator is None: - time.sleep(0.25) - - method = pika.spec.Basic.Deliver(consumer_tag=consumer_tag) - self.obj._on_consumer_generator_event(_ConsumerDeliveryEvt(method, properties, body)) - - def test_consume(self): consumed_deliveries = [] - def __consume(): + def __consume() -> None: for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): # Skip deliveries generated due to inactivity if delivery is not None and any(delivery): @@ -344,35 +193,17 @@ def __consume(): t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) + assert len(spans) == 0 - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) - - # Error logging - self.assertIsNone(rabbitmq_span.ec) + def test_consume_with_any_yielded(self, mocker) -> None: + mocker.patch( + "instana.instrumentation.pika.any", + return_value=False, + ) - # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) - - self.assertEqual(1, len(consumed_deliveries)) - - def test_consume_with_trace_context(self): consumed_deliveries = [] - def __consume(): + def __consume() -> None: for delivery in self.obj.consume("test.queue", inactivity_timeout=3.0): # Skip deliveries generated due to inactivity if delivery is not None and any(delivery): @@ -389,51 +220,45 @@ def __consume(): t = threading.Thread(target=__consume) t.start() - self._generate_delivery(consumer_tag, pika.BasicProperties(headers={ - "X-INSTANA-T": "0000000000000001", - "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }), "Hello!") + self._generate_delivery(consumer_tag, pika.BasicProperties(), "Hello!") t.join(timeout=5.0) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) - - rabbitmq_span = spans[0] - - self.assertIsNone(tracer.active_span) - - # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) - - # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) + assert len(spans) == 0 class TestPikaBlockingChannelBlockingConnection(_TestPika): - @mock.patch('pika.adapters.blocking_connection.BlockingConnection', autospec=True) - def _create_connection(self, connection=None): + @mock.patch("pika.adapters.blocking_connection.BlockingConnection", autospec=True) + def _create_connection(self, connection: Optional[mock.MagicMock] = None) -> object: connection._impl = mock.create_autospec(pika.connection.Connection) connection._impl.params = pika.connection.Parameters() return connection - @mock.patch('pika.channel.Channel', spec=pika.channel.Channel) - def _create_obj(self, channel_impl): + @mock.patch("pika.channel.Channel", spec=pika.channel.Channel) + def _create_obj( + self, channel_impl: mock.MagicMock + ) -> pika.adapters.blocking_connection.BlockingChannel: self.impl = channel_impl() self.impl.channel_number = 1 - return pika.adapters.blocking_connection.BlockingChannel(self.impl, self.connection) + return pika.adapters.blocking_connection.BlockingChannel( + self.impl, self.connection + ) - def _generate_delivery(self, method, properties, body): + def _generate_delivery( + self, + method: pika.spec.Basic.Deliver, + properties: pika.BasicProperties, + body: str, + ) -> None: from pika.adapters.blocking_connection import _ConsumerDeliveryEvt + evt = _ConsumerDeliveryEvt(method, properties, body) self.obj._add_pending_event(evt) self.obj._dispatch_events() - def test_basic_consume(self): + def test_basic_consume(self) -> None: consumer_tag = "test.consumer" self.impl.basic_consume.return_value = consumer_tag @@ -449,28 +274,26 @@ def test_basic_consume(self): self._generate_delivery(method, properties, body) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) - # A new span has been started - self.assertIsNotNone(rabbitmq_span.t) - self.assertIsNone(rabbitmq_span.p) - self.assertIsNotNone(rabbitmq_span.s) + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s # Error logging - self.assertIsNone(rabbitmq_span.ec) + assert not rabbitmq_span.ec # Span tags - self.assertIsNone(rabbitmq_span.data["rabbitmq"]["exchange"]) - self.assertEqual("consume", rabbitmq_span.data["rabbitmq"]["sort"]) - self.assertIsNotNone(rabbitmq_span.data["rabbitmq"]["address"]) - self.assertEqual("test.queue", rabbitmq_span.data["rabbitmq"]["queue"]) - self.assertIsNotNone(rabbitmq_span.stack) - self.assertTrue(type(rabbitmq_span.stack) is list) - self.assertGreater(len(rabbitmq_span.stack), 0) + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 cb.assert_called_once_with(self.obj, method, properties, body) @@ -485,25 +308,320 @@ def test_basic_consume_with_trace_context(self): self.obj.basic_consume(queue="test.queue", on_message_callback=cb) body = "Hello!" - properties = pika.BasicProperties(headers={ + instana_headers = { "X-INSTANA-T": "0000000000000001", "X-INSTANA-S": "0000000000000002", - "X-INSTANA-L": "1" - }) + "X-INSTANA-L": "1", + } + properties = pika.BasicProperties(headers=instana_headers) method = pika.spec.Basic.Deliver(consumer_tag) self._generate_delivery(method, properties, body) spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # Trace context propagation + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) + + # A new span has been started + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s + + +class TestPikaChannel(_TestPika): + def _create_obj(self) -> pika.channel.Channel: + return pika.channel.Channel(self.connection, 1, self._on_openok_callback) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish(self, send_method, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + with tracer.start_as_current_span("testing"): + self.obj.basic_publish("test.exchange", "test.queue", "Hello!") + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + rabbitmq_span = spans[0] + test_span = spans[1] + + # Same traceId + assert test_span.t == rabbitmq_span.t + + # Parent relationships + assert rabbitmq_span.p == test_span.s + + # Error logging + assert not test_span.ec + assert not rabbitmq_span.ec + + # Span tags + assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" + assert rabbitmq_span.data["rabbitmq"]["sort"] == "publish" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + send_method.assert_called_once_with( + pika.spec.Basic.Publish(exchange="test.exchange", routing_key="test.queue"), + ( + pika.spec.BasicProperties( + headers={ + "X-INSTANA-T": str(rabbitmq_span.t), + "X-INSTANA-S": str(rabbitmq_span.s), + "X-INSTANA-L": "1", + } + ), + b"Hello!", + ), + ) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish_as_root_exit_span(self, send_method, _unused) -> None: + agent.options.allow_exit_as_root = True + self.obj._set_state(self.obj.OPEN) + self.obj.basic_publish("test.exchange", "test.queue", "Hello!") + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # Parent relationships + assert not rabbitmq_span.p + + # Error logging + assert not rabbitmq_span.ec + + # Span tags + assert rabbitmq_span.data["rabbitmq"]["exchange"] == "test.exchange" + assert rabbitmq_span.data["rabbitmq"]["sort"] == "publish" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["key"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + send_method.assert_called_once_with( + pika.spec.Basic.Publish(exchange="test.exchange", routing_key="test.queue"), + ( + pika.spec.BasicProperties( + headers={ + "X-INSTANA-T": str(rabbitmq_span.t), + "X-INSTANA-S": str(rabbitmq_span.s), + "X-INSTANA-L": "1", + } + ), + b"Hello!", + ), + ) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish_with_headers(self, send_method, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + with tracer.start_as_current_span("testing"): + self.obj.basic_publish( + "test.exchange", + "test.queue", + "Hello!", + pika.BasicProperties(headers={"X-Custom-1": "test"}), + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 2 rabbitmq_span = spans[0] - self.assertIsNone(tracer.active_span) + send_method.assert_called_once_with( + pika.spec.Basic.Publish(exchange="test.exchange", routing_key="test.queue"), + ( + pika.spec.BasicProperties( + headers={ + "X-Custom-1": "test", + "X-INSTANA-T": str(rabbitmq_span.t), + "X-INSTANA-S": str(rabbitmq_span.s), + "X-INSTANA-L": "1", + } + ), + b"Hello!", + ), + ) + + @mock.patch("pika.spec.Basic.Publish") + @mock.patch("pika.channel.Channel._send_method") + def test_basic_publish_tracing_off(self, send_method, _unused, mocker) -> None: + mocker.patch( + "instana.instrumentation.pika.tracing_is_off", + return_value=True, + ) + + self.obj._set_state(self.obj.OPEN) + + with tracer.start_as_current_span("testing"): + self.obj.basic_publish("test.exchange", "test.queue", "Hello!") + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + # Span names are not "rabbitmq" + for span in spans: + assert span.n != "rabbitmq" + + @mock.patch("pika.spec.Basic.Get") + def test_basic_get(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + properties = pika.BasicProperties() + + method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_get("test.queue", cb) + self.obj._on_getok(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # A new span has been started + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s + + # Error logging + assert not rabbitmq_span.ec + + # Span tags + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + cb.assert_called_once_with(self.obj, pika.spec.Basic.GetOk, properties, body) + + @mock.patch("pika.spec.Basic.Get") + def test_basic_get_with_trace_context(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + instana_headers = { + "X-INSTANA-T": "0000000000000001", + "X-INSTANA-S": "0000000000000002", + "X-INSTANA-L": "1", + } + properties = pika.BasicProperties(headers=instana_headers) + + method_frame = pika.frame.Method(1, pika.spec.Basic.GetOk) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_get("test.queue", cb) + self.obj._on_getok(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # Trace context propagation + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) + + # A new span has been started + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s + + @mock.patch("pika.spec.Basic.Consume") + def test_basic_consume(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + properties = pika.BasicProperties() + + method_frame = pika.frame.Method( + 1, pika.spec.Basic.Deliver(consumer_tag="test") + ) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_consume("test.queue", cb, consumer_tag="test") + self.obj._on_deliver(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] + + # A new span has been started + assert rabbitmq_span.t + assert not rabbitmq_span.p + assert rabbitmq_span.s + + # Error logging + assert not rabbitmq_span.ec + + # Span tags + assert not rabbitmq_span.data["rabbitmq"]["exchange"] + assert rabbitmq_span.data["rabbitmq"]["sort"] == "consume" + assert rabbitmq_span.data["rabbitmq"]["address"] + assert rabbitmq_span.data["rabbitmq"]["queue"] == "test.queue" + assert rabbitmq_span.stack + assert isinstance(rabbitmq_span.stack, list) + assert len(rabbitmq_span.stack) > 0 + + cb.assert_called_once_with(self.obj, method_frame.method, properties, body) + + @mock.patch("pika.spec.Basic.Consume") + def test_basic_consume_with_trace_context(self, _unused) -> None: + self.obj._set_state(self.obj.OPEN) + + body = "Hello!" + instana_headers = { + "X-INSTANA-T": "0000000000000001", + "X-INSTANA-S": "0000000000000002", + "X-INSTANA-L": "1", + } + properties = pika.BasicProperties(headers=instana_headers) + + method_frame = pika.frame.Method( + 1, pika.spec.Basic.Deliver(consumer_tag="test") + ) + header_frame = pika.frame.Header(1, len(body), properties) + + cb = mock.Mock() + + self.obj.basic_consume( + queue="test.queue", on_message_callback=cb, consumer_tag="test" + ) + self.obj._on_deliver(method_frame, header_frame, body) + + spans = self.recorder.queued_spans() + assert len(spans) == 1 + + rabbitmq_span = spans[0] # Trace context propagation - self.assertEqual("0000000000000001", rabbitmq_span.t) - self.assertEqual("0000000000000002", rabbitmq_span.p) + assert rabbitmq_span.t == int(instana_headers["X-INSTANA-T"]) + assert rabbitmq_span.p == int(instana_headers["X-INSTANA-S"]) # A new span has been started - self.assertIsNotNone(rabbitmq_span.s) - self.assertNotEqual(rabbitmq_span.p, rabbitmq_span.s) + assert rabbitmq_span.s + assert rabbitmq_span.p != rabbitmq_span.s diff --git a/tests/conftest.py b/tests/conftest.py index 660f1245..e544b69d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,7 +38,6 @@ # TODO: remove the following entries as the migration of the instrumentation # codes are finalised. collect_ignore_glob.append("*clients/test_google*") -collect_ignore_glob.append("*clients/test_pika*") collect_ignore_glob.append("*clients/test_sql*") collect_ignore_glob.append("*frameworks/test_celery*") From c66ce2c6124afa6dc1a2e0a6e54f960d64a522e6 Mon Sep 17 00:00:00 2001 From: Paulo Vital Date: Thu, 19 Sep 2024 16:50:10 +0200 Subject: [PATCH 163/172] refactor: Remove TestAgent from the production code. Remove the TestAgent and the if-blocks that check if running in a test environment from the production code. Signed-off-by: Paulo Vital --- .circleci/config.yml | 1 - .tekton/run_unittests.sh | 1 - src/instana/agent/host.py | 3 -- src/instana/agent/test.py | 26 --------- src/instana/collector/aws_fargate.py | 5 -- src/instana/collector/base.py | 17 +----- src/instana/fsm.py | 5 +- src/instana/recorder.py | 7 --- src/instana/singletons.py | 11 +--- tests/__init__.py | 2 - tests/agent/test_host.py | 10 ++-- tests/apps/grpc_server/stan_server.py | 1 - tests/conftest.py | 77 ++++++++++++++++++++++----- tests/platforms/conftest.py | 18 +++++++ tests/test_tracer.py | 11 ++-- tests/test_tracer_provider.py | 4 +- 16 files changed, 93 insertions(+), 106 deletions(-) delete mode 100644 src/instana/agent/test.py create mode 100644 tests/platforms/conftest.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 758807c3..f62b7661 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,7 +56,6 @@ commands: - run: name: Run Tests With Coverage Report environment: - INSTANA_TEST: "true" CASSANDRA_TEST: "<>" COUCHBASE_TEST: "<>" GEVENT_STARLETTE_TEST: "<>" diff --git a/.tekton/run_unittests.sh b/.tekton/run_unittests.sh index dfcc79eb..fe91bb53 100755 --- a/.tekton/run_unittests.sh +++ b/.tekton/run_unittests.sh @@ -52,7 +52,6 @@ esac echo -n "Configuration is '${TEST_CONFIGURATION}' on ${PYTHON_VERSION} " echo "with dependencies in '${REQUIREMENTS}'" -export INSTANA_TEST='true' ls -lah . if [[ -n "${COUCHBASE_TEST}" ]]; then echo "Install Couchbase Dependencies" diff --git a/src/instana/agent/host.py b/src/instana/agent/host.py index 9bb3dd8e..b4943cdb 100644 --- a/src/instana/agent/host.py +++ b/src/instana/agent/host.py @@ -104,9 +104,6 @@ def can_send(self): Are we in a state where we can send data? @return: Boolean """ - if "INSTANA_TEST" in os.environ: - return True - # Watch for pid change (fork) self.last_fork_check = datetime.now() current_pid = os.getpid() diff --git a/src/instana/agent/test.py b/src/instana/agent/test.py deleted file mode 100644 index 06e70ec7..00000000 --- a/src/instana/agent/test.py +++ /dev/null @@ -1,26 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -""" -The in-process Instana agent (for testing & the test suite) that manages -monitoring state and reporting that data. -""" -import os -from ..log import logger -from .host import HostAgent - - -class TestAgent(HostAgent): - """ - Special Agent for the test suite. This agent is based on the StandardAgent. Overrides here are only for test - purposes and mocking. - """ - def get_from_structure(self): - """ - Retrieves the From data that is reported alongside monitoring data. - @return: dict() - """ - return {'e': os.getpid(), 'h': 'fake'} - - def report_traces(self, spans): - logger.warning("Tried to report_traces with a TestAgent!") diff --git a/src/instana/collector/aws_fargate.py b/src/instana/collector/aws_fargate.py index a8bc7e0a..323ca563 100644 --- a/src/instana/collector/aws_fargate.py +++ b/src/instana/collector/aws_fargate.py @@ -19,7 +19,6 @@ from instana.collector.helpers.runtime import RuntimeHelper from instana.collector.utils import format_span from instana.log import logger -from instana.singletons import env_is_test from instana.util import DictionaryOfStan, validate_url @@ -100,10 +99,6 @@ def get_ecs_metadata(self): Get the latest data from the ECS metadata container API and store on the class @return: Boolean """ - if env_is_test is True: - # For test, we are using mock ECS metadata - return - try: self.fetching_start_time = int(time()) delta = self.fetching_start_time - self.last_ecmu_full_fetch diff --git a/src/instana/collector/base.py b/src/instana/collector/base.py index 8c6df344..67008e34 100644 --- a/src/instana/collector/base.py +++ b/src/instana/collector/base.py @@ -8,14 +8,10 @@ import queue # pylint: disable=import-error import threading -from os import environ from instana.log import logger from instana.util import DictionaryOfStan, every -# TODO: Use mock.patch() or unittest.mock to mock the testing env -env_is_test = "INSTANA_TEST" in environ - class BaseCollector(object): """ @@ -31,16 +27,7 @@ def __init__(self, agent): self.THREAD_NAME = "Instana Collector" # The Queue where we store finished spans before they are sent - if env_is_test: - # Override span queue with a multiprocessing version - # The test suite runs background applications - some in background threads, - # others in background processes. This multiprocessing queue allows us to collect - # up spans from all sources. - import multiprocessing - - self.span_queue = multiprocessing.Queue() - else: - self.span_queue = queue.Queue() + self.span_queue = queue.Queue() # The Queue where we store finished profiles before they are sent self.profile_queue = queue.Queue() @@ -163,8 +150,6 @@ def prepare_and_report_data(self): Prepare and report the data payload. @return: Boolean """ - if env_is_test: - return True with self.background_report_lock: payload = self.prepare_payload() self.agent.report_data_payload(payload) diff --git a/src/instana/fsm.py b/src/instana/fsm.py index 3cdd25ee..1897cf30 100644 --- a/src/instana/fsm.py +++ b/src/instana/fsm.py @@ -67,10 +67,7 @@ def __init__(self, agent): self.timer = threading.Timer(1, self.fsm.lookup) self.timer.daemon = True self.timer.name = self.THREAD_NAME - - # Only start the announce process when not in Test - if not "INSTANA_TEST" in os.environ: - self.timer.start() + self.timer.start() @staticmethod def print_state_change(e): diff --git a/src/instana/recorder.py b/src/instana/recorder.py index f14e9a60..9ec882f7 100644 --- a/src/instana/recorder.py +++ b/src/instana/recorder.py @@ -41,13 +41,6 @@ def queued_spans(self) -> List[ReadableSpan]: span = None spans = [] - import time - - from .singletons import env_is_test - - if env_is_test is True: - time.sleep(1) - if self.agent.collector.span_queue.empty() is True: return spans diff --git a/src/instana/singletons.py b/src/instana/singletons.py index 0d04f22a..9cc328ac 100644 --- a/src/instana/singletons.py +++ b/src/instana/singletons.py @@ -15,7 +15,6 @@ # Detect the environment where we are running ahead of time aws_env = os.environ.get("AWS_EXECUTION_ENV", "") -env_is_test = "INSTANA_TEST" in os.environ env_is_aws_fargate = aws_env == "AWS_ECS_FARGATE" env_is_aws_eks_fargate = ( os.environ.get("INSTANA_TRACER_ENVIRONMENT") == "AWS_EKS_FARGATE" @@ -29,15 +28,7 @@ (k_service, k_configuration, k_revision, instana_endpoint_url) ) -if env_is_test: - from .agent.test import TestAgent - from .recorder import StanRecorder - - agent = TestAgent() - span_recorder = StanRecorder(agent) - profiler = Profiler(agent) - -elif env_is_aws_lambda: +if env_is_aws_lambda: from .agent.aws_lambda import AWSLambdaAgent from .recorder import StanRecorder diff --git a/tests/__init__.py b/tests/__init__.py index 81660d27..39799ddb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,8 +3,6 @@ import os -os.environ["INSTANA_TEST"] = "true" - if os.environ.get('GEVENT_STARLETTE_TEST'): from gevent import monkey monkey.patch_all() diff --git a/tests/agent/test_host.py b/tests/agent/test_host.py index e27e361e..b345ce28 100644 --- a/tests/agent/test_host.py +++ b/tests/agent/test_host.py @@ -71,13 +71,7 @@ def test_is_timed_out(): assert agent.is_timed_out() -def test_can_send_test_env(): - agent = HostAgent() - with patch.dict("os.environ", {"INSTANA_TEST": "sample-data"}): - if "INSTANA_TEST" in os.environ: - assert agent.can_send() - - +@pytest.mark.original def test_can_send(): agent = HostAgent() agent._boot_pid = 12345 @@ -92,6 +86,7 @@ def test_can_send(): assert agent.can_send() is True +@pytest.mark.original def test_can_send_default(): agent = HostAgent() with patch.dict("os.environ", {}, clear=True): @@ -121,6 +116,7 @@ def test_set_from(): assert agent.announce_data.pid == 1234 +@pytest.mark.original def test_get_from_structure(): agent = HostAgent() agent.announce_data = AnnounceData(pid=1234, agentUuid="value") diff --git a/tests/apps/grpc_server/stan_server.py b/tests/apps/grpc_server/stan_server.py index 60c446f4..e69de2a6 100644 --- a/tests/apps/grpc_server/stan_server.py +++ b/tests/apps/grpc_server/stan_server.py @@ -92,7 +92,6 @@ def start_server(self): if __name__ == "__main__": print ("Booting foreground GRPC application...") - # os.environ["INSTANA_TEST"] = "true" if sys.version_info >= (3, 5, 3): StanServicer().start_server() diff --git a/tests/conftest.py b/tests/conftest.py index e544b69d..794b8d3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import importlib.util import os import sys +from typing import Any, Dict import pytest from opentelemetry.context.context import Context @@ -12,18 +13,13 @@ if importlib.util.find_spec("celery"): pytest_plugins = ("celery.contrib.pytest",) -# Set our testing flags -os.environ["INSTANA_TEST"] = "true" - -# TODO: remove all "noqa: E402" from instana package imports and move the -# block of env variables setting to below the imports after finishing the -# migration of instrumentation codes. -from instana.agent.test import TestAgent # noqa: E402 -from instana.recorder import StanRecorder # noqa: E402 -from instana.span.base_span import BaseSpan # noqa: E402 -from instana.span.span import InstanaSpan # noqa: E402 -from instana.span_context import SpanContext # noqa: E402 -from instana.tracer import InstanaTracerProvider # noqa: E402 +from instana.agent.host import HostAgent +from instana.collector.base import BaseCollector +from instana.recorder import StanRecorder +from instana.span.base_span import BaseSpan +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext +from instana.tracer import InstanaTracerProvider # Ignoring tests during OpenTelemetry migration. collect_ignore_glob = [ @@ -129,14 +125,14 @@ def span_id() -> int: @pytest.fixture def span_processor() -> StanRecorder: - rec = StanRecorder(TestAgent()) + rec = StanRecorder(HostAgent()) rec.THREAD_NAME = "InstanaSpan Recorder Test" return rec @pytest.fixture def tracer_provider(span_processor: StanRecorder) -> InstanaTracerProvider: - return InstanaTracerProvider(span_processor=span_processor, exporter=TestAgent()) + return InstanaTracerProvider(span_processor=span_processor, exporter=HostAgent()) @pytest.fixture @@ -162,3 +158,56 @@ def base_span(span: InstanaSpan) -> BaseSpan: @pytest.fixture def context(span: InstanaSpan) -> Context: return set_span_in_context(span) + + +def always_true(_: object) -> bool: + return True + + +# Mocking HostAgent.can_send() +@pytest.fixture(autouse=True) +def can_send(monkeypatch, request) -> None: + """Return always True for HostAgent.can_send()""" + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original HostAgent.can_send() + monkeypatch.setattr(HostAgent, "can_send", HostAgent.can_send) + else: + monkeypatch.setattr(HostAgent, "can_send", always_true) + + +# Mocking HostAgent.get_from_structure() +@pytest.fixture(autouse=True) +def get_from_structure(monkeypatch, request) -> None: + """ + Retrieves the From data that is reported alongside monitoring data. + @return: dict() + """ + + def _get_from_structure(_: object) -> Dict[str, Any]: + return {"e": os.getpid(), "h": "fake"} + + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original HostAgent.get_from_structure() + monkeypatch.setattr( + HostAgent, "get_from_structure", HostAgent.get_from_structure + ) + else: + monkeypatch.setattr(HostAgent, "get_from_structure", _get_from_structure) + + +# Mocking BaseCollector.prepare_and_report_data() +@pytest.fixture(autouse=True) +def prepare_and_report_data(monkeypatch, request): + """Return always True for BaseCollector.prepare_and_report_data()""" + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original BaseCollector.prepare_and_report_data() + monkeypatch.setattr( + BaseCollector, + "prepare_and_report_data", + BaseCollector.prepare_and_report_data, + ) + else: + monkeypatch.setattr(BaseCollector, "prepare_and_report_data", always_true) diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py new file mode 100644 index 00000000..115e93ad --- /dev/null +++ b/tests/platforms/conftest.py @@ -0,0 +1,18 @@ +import pytest + +from instana.collector.aws_fargate import AWSFargateCollector + +# Mocking AWSFargateCollector.get_ecs_metadata() +@pytest.fixture(autouse=True) +def get_ecs_metadata(monkeypatch, request) -> None: + """Return always True for AWSFargateCollector.get_ecs_metadata()""" + + def _always_true(_: object) -> bool: + return True + + if "original" in request.keywords: + # If using the `@pytest.mark.original` marker before the test function, + # uses the original AWSFargateCollector.get_ecs_metadata() + monkeypatch.setattr(AWSFargateCollector, "get_ecs_metadata", AWSFargateCollector.get_ecs_metadata) + else: + monkeypatch.setattr(AWSFargateCollector, "get_ecs_metadata", _always_true) diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 73d69ea4..06474447 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,22 +1,19 @@ # (c) Copyright IBM Corp. 2024 import pytest - from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE -from instana.agent.test import TestAgent +from instana.agent.host import HostAgent from instana.recorder import StanRecorder from instana.sampling import InstanaSampler from instana.span.span import ( + INVALID_SPAN, + INVALID_SPAN_ID, InstanaSpan, get_current_span, - INVALID_SPAN_ID, - INVALID_SPAN, ) from instana.span_context import SpanContext from instana.tracer import InstanaTracer, InstanaTracerProvider -from opentelemetry.context.context import Context -from opentelemetry.trace.span import _SPAN_ID_MAX_VALUE, INVALID_SPAN_ID def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: @@ -29,7 +26,7 @@ def test_tracer_defaults(tracer_provider: InstanaTracerProvider) -> None: assert isinstance(tracer._sampler, InstanaSampler) assert isinstance(tracer.span_processor, StanRecorder) - assert isinstance(tracer.exporter, TestAgent) + assert isinstance(tracer.exporter, HostAgent) assert len(tracer._propagators) == 3 diff --git a/tests/test_tracer_provider.py b/tests/test_tracer_provider.py index 2a55203b..5a1ffd5b 100644 --- a/tests/test_tracer_provider.py +++ b/tests/test_tracer_provider.py @@ -2,8 +2,8 @@ from pytest import LogCaptureFixture +from instana.agent.base import BaseAgent from instana.agent.host import HostAgent -from instana.agent.test import TestAgent from instana.propagators.binary_propagator import BinaryPropagator from instana.propagators.format import Format from instana.propagators.http_propagator import HTTPPropagator @@ -49,5 +49,5 @@ def test_tracer_provider_add_span_processor(span_processor: StanRecorder) -> Non provider.add_span_processor(span_processor) assert isinstance(provider._span_processor, StanRecorder) - assert isinstance(provider._span_processor.agent, TestAgent) + assert isinstance(provider._span_processor.agent, BaseAgent) assert provider._span_processor.THREAD_NAME == "InstanaSpan Recorder Test" From 84001106ac729f7a3cfa8bdde59a4fa2b16ecb30 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 18 Sep 2024 14:13:21 +0300 Subject: [PATCH 164/172] refactor(sqlalchemy): added sqlalchemy otel instrumentation --- src/instana/__init__.py | 2 +- src/instana/instrumentation/sqlalchemy.py | 145 +++++++++++++--------- 2 files changed, 88 insertions(+), 59 deletions(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 647b6253..0801367a 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -179,7 +179,7 @@ def boot_agent(): pymongo, # noqa: F401 pymysql, # noqa: F401 redis, # noqa: F401 - # sqlalchemy, # noqa: F401 + sqlalchemy, # noqa: F401 starlette_inst, # noqa: F401 sanic_inst, # noqa: F401 urllib3, # noqa: F401 diff --git a/src/instana/instrumentation/sqlalchemy.py b/src/instana/instrumentation/sqlalchemy.py index 2adf705a..3f44b526 100644 --- a/src/instana/instrumentation/sqlalchemy.py +++ b/src/instana/instrumentation/sqlalchemy.py @@ -3,90 +3,119 @@ import re -from operator import attrgetter +from typing import Any, Dict -from ..log import logger -from ..util.traceutils import get_tracer_tuple, tracing_is_off +from opentelemetry import context, trace + +from instana.log import logger +from instana.span.span import InstanaSpan, get_current_span +from instana.span_context import SpanContext +from instana.util.traceutils import get_tracer_tuple, tracing_is_off try: - import sqlalchemy + from sqlalchemy import __version__ as sqlalchemy_version from sqlalchemy import event from sqlalchemy.engine import Engine url_regexp = re.compile(r"\/\/(\S+@)") - - @event.listens_for(Engine, 'before_cursor_execute', named=True) - def receive_before_cursor_execute(**kw): + @event.listens_for(Engine, "before_cursor_execute", named=True) + def receive_before_cursor_execute( + **kw: Dict[str, Any], + ) -> None: try: # If we're not tracing, just return if tracing_is_off(): return tracer, parent_span, _ = get_tracer_tuple() - scope = tracer.start_active_span("sqlalchemy", child_of=parent_span) - context = kw['context'] - if context: - context._stan_scope = scope - - conn = kw['conn'] - url = str(conn.engine.url) - scope.span.set_tag('sqlalchemy.sql', kw['statement']) - scope.span.set_tag('sqlalchemy.eng', conn.engine.name) - scope.span.set_tag('sqlalchemy.url', url_regexp.sub('//', url)) - except Exception as e: - logger.debug(e) - return - - - @event.listens_for(Engine, 'after_cursor_execute', named=True) - def receive_after_cursor_execute(**kw): - context = kw['context'] - - if context is not None and hasattr(context, '_stan_scope'): - scope = context._stan_scope - if scope is not None: - scope.close() + parent_context = parent_span.get_span_context() if parent_span else None + + span = tracer.start_span("sqlalchemy", span_context=parent_context) + conn = kw["conn"] + conn.span = span + span.set_attribute("sqlalchemy.sql", kw["statement"]) + span.set_attribute("sqlalchemy.eng", conn.engine.name) + span.set_attribute( + "sqlalchemy.url", url_regexp.sub("//", str(conn.engine.url)) + ) + + ctx = trace.set_span_in_context(span) + token = context.attach(ctx) + conn.token = token + except Exception: + logger.debug( + "Instrumenting sqlalchemy @ receive_before_cursor_execute", + exc_info=True, + ) + + @event.listens_for(Engine, "after_cursor_execute", named=True) + def receive_after_cursor_execute( + **kw: Dict[str, Any], + ) -> None: + try: + # If we're not tracing, just return + if tracing_is_off(): + return + current_span = get_current_span() + conn = kw["conn"] + if current_span.is_recording(): + current_span.end() + if hasattr(conn, "token"): + context.detach(conn.token) + conn.token = None + except Exception: + logger.debug( + "Instrumenting sqlalchemy @ receive_after_cursor_execute", + exc_info=True, + ) error_event = "handle_error" # Handle dbapi_error event; deprecated since version 0.9 - if sqlalchemy.__version__[0] == "0": + if sqlalchemy_version[0] == "0": error_event = "dbapi_error" - - def _set_error_tags(context, exception_string, scope_string): - scope, context_exception = None, None - if attrgetter(scope_string)(context) and attrgetter(exception_string)(context): - scope = attrgetter(scope_string)(context) - context_exception = attrgetter(exception_string)(context) - if scope and context_exception: - scope.span.log_exception(context_exception) - scope.close() + def _set_error_attributes( + context: SpanContext, + exception_string: str, + span: InstanaSpan, + ) -> None: + context_exception = None, None + if hasattr(context, exception_string): + context_exception = getattr(context, exception_string) + if span and context_exception: + span.record_exception(context_exception) else: - scope.span.log_exception("No %s specified." % error_event) - scope.close() - + span.record_exception(f"No {error_event} specified.") + if span.is_recording(): + span.end() @event.listens_for(Engine, error_event, named=True) - def receive_handle_db_error(**kw): - - if tracing_is_off(): - return + def receive_handle_db_error( + **kw: Dict[str, Any], + ) -> None: + try: + if tracing_is_off(): + return - # support older db error event - if error_event == "dbapi_error": - context = kw.get('context') - exception_string = 'exception' - scope_string = '_stan_scope' - else: - context = kw.get('exception_context') - exception_string = 'sqlalchemy_exception' - scope_string = 'execution_context._stan_scope' + current_span = get_current_span() - if context: - _set_error_tags(context, exception_string, scope_string) + # support older db error event + if error_event == "dbapi_error": + context = kw.get("context") + exception_string = "exception" + else: + context = kw.get("exception_context") + exception_string = "sqlalchemy_exception" + if context: + _set_error_attributes(context, exception_string, current_span) + except Exception: + logger.debug( + "Instrumenting sqlalchemy @ receive_handle_db_error", + exc_info=True, + ) logger.debug("Instrumenting sqlalchemy") From 81c7df2e0aaf4a32f35cfb9af6a2686aa956de82 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 18 Sep 2024 14:13:53 +0300 Subject: [PATCH 165/172] unittest(sqlalchemy): added unittests of sqlalchemy otel instrumentation --- tests/clients/test_sqlalchemy.py | 361 ++++++++++++++++++------------- tests/conftest.py | 1 - 2 files changed, 207 insertions(+), 155 deletions(-) diff --git a/tests/clients/test_sqlalchemy.py b/tests/clients/test_sqlalchemy.py index 68afa1ec..6aef6fc9 100644 --- a/tests/clients/test_sqlalchemy.py +++ b/tests/clients/test_sqlalchemy.py @@ -1,244 +1,297 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest +from typing import Generator -from ..helpers import testenv -from instana.singletons import agent, tracer - -from sqlalchemy.orm import sessionmaker -from sqlalchemy.exc import OperationalError -from sqlalchemy.orm import declarative_base +import pytest from sqlalchemy import Column, Integer, String, create_engine, text +from sqlalchemy.exc import OperationalError +from sqlalchemy.orm import declarative_base, sessionmaker +from instana.singletons import agent, tracer +from instana.span.span import get_current_span +from tests.helpers import testenv + +engine = create_engine( + f"postgresql://{testenv['postgresql_user']}:{testenv['postgresql_pw']}@{testenv['postgresql_host']}:{testenv['postgresql_port']}/{testenv['postgresql_db']}" +) -engine = create_engine("postgresql://%s:%s@%s/%s" % (testenv['postgresql_user'], testenv['postgresql_pw'], - testenv['postgresql_host'], testenv['postgresql_db'])) +Session = sessionmaker(bind=engine) Base = declarative_base() + class StanUser(Base): - __tablename__ = 'churchofstan' + __tablename__ = "churchofstan" - id = Column(Integer, primary_key=True) - name = Column(String) - fullname = Column(String) - password = Column(String) + id = Column(Integer, primary_key=True) + name = Column(String) + fullname = Column(String) + password = Column(String) - def __repr__(self): + def __repr__(self) -> None: return "" % ( - self.name, self.fullname, self.password) - -Base.metadata.create_all(engine) - -stan_user = StanUser(name='IAmStan', fullname='Stan Robot', password='3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR') -stan_user2 = StanUser(name='IAmStanToo', fullname='Stan Robot 2', password='3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR') - -Session = sessionmaker(bind=engine) -Session.configure(bind=engine) - -sqlalchemy_url = 'postgresql://%s/%s' % (testenv['postgresql_host'], testenv['postgresql_db']) - - -class TestSQLAlchemy(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ - self.recorder = tracer.recorder + self.name, + self.fullname, + self.password, + ) + + +@pytest.fixture(scope="class") +def db_setup() -> None: + with tracer.start_as_current_span("metadata") as span: + Base.metadata.create_all(engine) + span.end() + + +stan_user = StanUser( + name="IAmStan", + fullname="Stan Robot", + password="3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR", +) +stan_user2 = StanUser( + name="IAmStanToo", + fullname="Stan Robot 2", + password="3X}vP66ADoCFT2g?HPvoem2eJh,zWXgd36Rb/{aRq/>7EYy6@EEH4BP(oeXac@mR", +) + +sqlalchemy_url = f"postgresql://{testenv['postgresql_host']}:{testenv['postgresql_port']}/{testenv['postgresql_db']}" + + +@pytest.mark.usefixtures("db_setup") +class TestSQLAlchemy: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" + self.recorder = tracer.span_processor self.recorder.clear_spans() self.session = Session() - - def tearDown(self): - """ Ensure that allow_exit_as_root has the default value """ + yield + """Ensure that allow_exit_as_root has the default value""" + self.session.close() agent.options.allow_exit_as_root = False - def test_session_add(self): - with tracer.start_active_span('test'): + def test_session_add(self) -> None: + with tracer.start_as_current_span("test"): self.session.add(stan_user) self.session.commit() spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) sql_span = spans[0] test_span = spans[1] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, sql_span.t) + assert sql_span.t == test_span.t # Parent relationships - self.assertEqual(sql_span.p, test_span.s) + assert sql_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(sql_span.ec) + assert not test_span.ec + assert not sql_span.ec # SQLAlchemy span - self.assertEqual('sqlalchemy', sql_span.n) - self.assertFalse('custom' in sql_span.data) - self.assertTrue('sqlalchemy' in sql_span.data) - - self.assertEqual('postgresql', sql_span.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span.data["sqlalchemy"]["url"]) - self.assertEqual('INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id', sql_span.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span.data["sqlalchemy"]["err"]) - - self.assertIsNotNone(sql_span.stack) - self.assertTrue(type(sql_span.stack) is list) - self.assertGreater(len(sql_span.stack), 0) - - def test_session_add_as_root_exit_span(self): + assert sql_span.n == "sqlalchemy" + assert "custom" not in sql_span.data + assert "sqlalchemy" in sql_span.data + + assert sql_span.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span.data["sqlalchemy"]["url"] + assert ( + "INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id" + == sql_span.data["sqlalchemy"]["sql"] + ) + assert not sql_span.data["sqlalchemy"]["err"] + + assert sql_span.stack + assert isinstance(sql_span.stack, list) + assert len(sql_span.stack) > 0 + + def test_session_add_as_root_exit_span(self) -> None: agent.options.allow_exit_as_root = True self.session.add(stan_user2) self.session.commit() spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 sql_span = spans[0] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Parent relationships - self.assertEqual(sql_span.p, None) + assert not sql_span.p # Error logging - self.assertIsNone(sql_span.ec) + assert not sql_span.ec # SQLAlchemy span - self.assertEqual('sqlalchemy', sql_span.n) - self.assertFalse('custom' in sql_span.data) - self.assertTrue('sqlalchemy' in sql_span.data) - - self.assertEqual('postgresql', sql_span.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span.data["sqlalchemy"]["url"]) - self.assertEqual('INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id', sql_span.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span.data["sqlalchemy"]["err"]) - - self.assertIsNotNone(sql_span.stack) - self.assertTrue(type(sql_span.stack) is list) - self.assertGreater(len(sql_span.stack), 0) - - def test_transaction(self): - result = None - with tracer.start_active_span('test'): + assert sql_span.n == "sqlalchemy" + assert "custom" not in sql_span.data + assert "sqlalchemy" in sql_span.data + + assert sql_span.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span.data["sqlalchemy"]["url"] + assert ( + "INSERT INTO churchofstan (name, fullname, password) VALUES (%(name)s, %(fullname)s, %(password)s) RETURNING churchofstan.id" + == sql_span.data["sqlalchemy"]["sql"] + ) + assert not sql_span.data["sqlalchemy"]["err"] + + assert sql_span.stack + assert isinstance(sql_span.stack, list) + assert len(sql_span.stack) > 0 + + def test_transaction(self) -> None: + with tracer.start_as_current_span("test"): with engine.begin() as connection: - result = connection.execute(text("select 1")) - result = connection.execute(text("select (name, fullname, password) from churchofstan where name='doesntexist'")) + connection.execute(text("select 1")) + connection.execute( + text( + "select (name, fullname, password) from churchofstan where name='doesntexist'" + ) + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 sql_span0 = spans[0] sql_span1 = spans[1] test_span = spans[2] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, sql_span0.t) - self.assertEqual(test_span.t, sql_span1.t) + assert sql_span0.t == test_span.t + assert sql_span1.t == test_span.t # Parent relationships - self.assertEqual(sql_span0.p, test_span.s) - self.assertEqual(sql_span1.p, test_span.s) + assert sql_span0.p == test_span.s + assert sql_span1.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(sql_span0.ec) - self.assertIsNone(sql_span1.ec) + assert not test_span.ec + assert not sql_span0.ec + assert not sql_span1.ec # SQLAlchemy span0 - self.assertEqual('sqlalchemy', sql_span0.n) - self.assertFalse('custom' in sql_span0.data) - self.assertTrue('sqlalchemy' in sql_span0.data) + assert sql_span0.n == "sqlalchemy" + assert "custom" not in sql_span0.data + assert "sqlalchemy" in sql_span0.data - self.assertEqual('postgresql', sql_span0.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span0.data["sqlalchemy"]["url"]) - self.assertEqual('select 1', sql_span0.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span0.data["sqlalchemy"]["err"]) + assert sql_span0.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span0.data["sqlalchemy"]["url"] + assert sql_span0.data["sqlalchemy"]["sql"] == "select 1" + assert not sql_span0.data["sqlalchemy"]["err"] - self.assertIsNotNone(sql_span0.stack) - self.assertTrue(type(sql_span0.stack) is list) - self.assertGreater(len(sql_span0.stack), 0) + assert sql_span0.stack + assert isinstance(sql_span0.stack, list) + assert len(sql_span0.stack) > 0 # SQLAlchemy span1 - self.assertEqual('sqlalchemy', sql_span1.n) - self.assertFalse('custom' in sql_span1.data) - self.assertTrue('sqlalchemy' in sql_span1.data) - - self.assertEqual('postgresql', sql_span1.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span1.data["sqlalchemy"]["url"]) - self.assertEqual("select (name, fullname, password) from churchofstan where name='doesntexist'", sql_span1.data["sqlalchemy"]["sql"]) - self.assertIsNone(sql_span1.data["sqlalchemy"]["err"]) - - self.assertIsNotNone(sql_span1.stack) - self.assertTrue(type(sql_span1.stack) is list) - self.assertGreater(len(sql_span1.stack), 0) - - def test_error_logging(self): - with tracer.start_active_span('test'): + assert sql_span1.n == "sqlalchemy" + assert "custom" not in sql_span1.data + assert "sqlalchemy" in sql_span1.data + + assert sql_span1.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span1.data["sqlalchemy"]["url"] + assert ( + "select (name, fullname, password) from churchofstan where name='doesntexist'" + == sql_span1.data["sqlalchemy"]["sql"] + ) + assert not sql_span1.data["sqlalchemy"]["err"] + + assert sql_span1.stack + assert isinstance(sql_span1.stack, list) + assert len(sql_span1.stack) > 0 + + def test_error_logging(self) -> None: + with tracer.start_as_current_span("test"): try: self.session.execute(text("htVwGrCwVThisIsInvalidSQLaw4ijXd88")) - self.session.commit() - except: + # self.session.commit() + except Exception: pass spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) + assert len(spans) == 2 sql_span = spans[0] test_span = spans[1] - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() # Same traceId - self.assertEqual(test_span.t, sql_span.t) + assert sql_span.t == test_span.t # Parent relationships - self.assertEqual(sql_span.p, test_span.s) + assert sql_span.p == test_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertIs(sql_span.ec, 1) + assert not test_span.ec + assert sql_span.ec == 1 # SQLAlchemy span - self.assertEqual('sqlalchemy', sql_span.n) - - self.assertFalse('custom' in sql_span.data) - self.assertTrue('sqlalchemy' in sql_span.data) - - self.assertEqual('postgresql', sql_span.data["sqlalchemy"]["eng"]) - self.assertEqual(sqlalchemy_url, sql_span.data["sqlalchemy"]["url"]) - self.assertEqual('htVwGrCwVThisIsInvalidSQLaw4ijXd88', sql_span.data["sqlalchemy"]["sql"]) - self.assertIn('syntax error at or near "htVwGrCwVThisIsInvalidSQLaw4ijXd88', sql_span.data["sqlalchemy"]["err"]) - self.assertIsNotNone(sql_span.stack) - self.assertTrue(type(sql_span.stack) is list) - self.assertGreater(len(sql_span.stack), 0) - - def test_error_before_tracing(self): + assert sql_span.n == "sqlalchemy" + + assert "custom" not in sql_span.data + assert "sqlalchemy" in sql_span.data + + assert sql_span.data["sqlalchemy"]["eng"] == "postgresql" + assert sqlalchemy_url == sql_span.data["sqlalchemy"]["url"] + assert ( + "htVwGrCwVThisIsInvalidSQLaw4ijXd88" == sql_span.data["sqlalchemy"]["sql"] + ) + assert ( + 'syntax error at or near "htVwGrCwVThisIsInvalidSQLaw4ijXd88' + in sql_span.data["sqlalchemy"]["err"] + ) + assert sql_span.stack + assert isinstance(sql_span.stack, list) + assert len(sql_span.stack) > 0 + + def test_error_before_tracing(self) -> None: """Test the scenario, in which instana is loaded, - but connection fails before tracing begins. - This is typical in test container scenario, - where it is "normal" to just start hammering a database container - which is still starting and not ready to handle requests yet. - In this scenario it is important that we get - an sqlalachemy exception, and not something else - like an AttributeError. Because testcontainer has a logic - to retry in case of certain sqlalchemy exceptions but it - can't handle an AttributeError.""" + but connection fails before tracing begins. + This is typical in test container scenario, + where it is "normal" to just start hammering a database container + which is still starting and not ready to handle requests yet. + In this scenario it is important that we get + an sqlalachemy exception, and not something else + like an AttributeError. Because testcontainer has a logic + to retry in case of certain sqlalchemy exceptions but it + can't handle an AttributeError.""" # https://github.com/instana/python-sensor/issues/362 - self.assertIsNone(tracer.active_span) + current_span = get_current_span() + assert not current_span.is_recording() - invalid_connection_url = 'postgresql://user1:pwd1@localhost:9999/mydb1' - with self.assertRaisesRegex( - OperationalError, - r'\(psycopg2.OperationalError\) connection .* failed.*' - ) as context_manager: + invalid_connection_url = "postgresql://user1:pwd1@localhost:9999/mydb1" + with pytest.raises( + OperationalError, + match=r"\(psycopg2.OperationalError\) connection .* failed.*", + ) as context_manager: engine = create_engine(invalid_connection_url) with engine.connect() as connection: - version, = connection.execute(text("select version()")).fetchone() - - the_exception = context_manager.exception - self.assertFalse(the_exception.connection_invalidated) + (version,) = connection.execute(text("select version()")).fetchone() + + the_exception = context_manager.value + assert not the_exception.connection_invalidated + + def test_if_not_tracing(self) -> None: + with engine.begin() as connection: + connection.execute(text("select 1")) + connection.execute( + text( + "select (name, fullname, password) from churchofstan where name='doesntexist'" + ) + ) + + current_span = get_current_span() + assert not current_span.is_recording() diff --git a/tests/conftest.py b/tests/conftest.py index 794b8d3d..c7c7cb8b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,7 +34,6 @@ # TODO: remove the following entries as the migration of the instrumentation # codes are finalised. collect_ignore_glob.append("*clients/test_google*") -collect_ignore_glob.append("*clients/test_sql*") collect_ignore_glob.append("*frameworks/test_celery*") collect_ignore_glob.append("*frameworks/test_gevent*") From 0a63b0b9a1514dd5cb86ff5c3491febecefb48ea Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Fri, 20 Sep 2024 18:53:06 +0300 Subject: [PATCH 166/172] added cassandra instrumentation --- src/instana/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 0801367a..f81e5815 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -165,7 +165,7 @@ def boot_agent(): from instana.instrumentation import ( asyncio, # noqa: F401 boto3_inst, # noqa: F401 - # cassandra_inst, # noqa: F401 + cassandra_inst, # noqa: F401 couchbase_inst, # noqa: F401 fastapi_inst, # noqa: F401 flask, # noqa: F401 From 4de8c0c479e7e4002ce6312291047fdded4dc5a8 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Thu, 12 Sep 2024 18:00:42 +0530 Subject: [PATCH 167/172] refactor: pyramid tween instrumentation Signed-off-by: Varsha GS (cherry picked from commit 8ab3210a92502253876f91213009c287167f6790) --- src/instana/instrumentation/pyramid/tweens.py | 155 ++++++++++-------- 1 file changed, 86 insertions(+), 69 deletions(-) diff --git a/src/instana/instrumentation/pyramid/tweens.py b/src/instana/instrumentation/pyramid/tweens.py index 5f6c0d11..aed34e63 100644 --- a/src/instana/instrumentation/pyramid/tweens.py +++ b/src/instana/instrumentation/pyramid/tweens.py @@ -3,90 +3,107 @@ from pyramid.httpexceptions import HTTPException +from typing import TYPE_CHECKING, Dict, Any, Callable -import opentracing as ot -import opentracing.ext.tags as ext +from opentelemetry.semconv.trace import SpanAttributes +from opentelemetry.trace import SpanKind -from ...log import logger -from ...singletons import tracer, agent -from ...util.secrets import strip_secrets_from_query +from instana.log import logger +from instana.singletons import tracer, agent +from instana.util.secrets import strip_secrets_from_query +from instana.propagators.format import Format + +if TYPE_CHECKING: + from pyramid.request import Request + from pyramid.response import Response + from pyramid.config import Configurator + from instana.span.span import InstanaSpan + from pyramid.registry import Registry class InstanaTweenFactory(object): """A factory that provides Instana instrumentation tween for Pyramid apps""" - def __init__(self, handler, registry): + def __init__( + self, handler: Callable[["Request"], "Response"], registry: "Registry" + ) -> None: self.handler = handler - def _extract_custom_headers(self, span, headers): - if agent.options.extra_http_headers is None: + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any] + ) -> None: + if not agent.options.extra_http_headers: return try: for custom_header in agent.options.extra_http_headers: if custom_header in headers: - span.set_tag("http.header.%s" % custom_header, headers[custom_header]) + span.set_attribute( + "http.header.%s" % custom_header, headers[custom_header] + ) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) - def __call__(self, request): - ctx = tracer.extract(ot.Format.HTTP_HEADERS, dict(request.headers)) - scope = tracer.start_active_span('http', child_of=ctx) - - scope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER) - scope.span.set_tag("http.host", request.host) - scope.span.set_tag(ext.HTTP_METHOD, request.method) - scope.span.set_tag(ext.HTTP_URL, request.path) - - if request.matched_route is not None: - scope.span.set_tag("http.path_tpl", request.matched_route.pattern) - - self._extract_custom_headers(scope.span, request.headers) - - if len(request.query_string): - scrubbed_params = strip_secrets_from_query(request.query_string, agent.options.secrets_matcher, - agent.options.secrets_list) - scope.span.set_tag("http.params", scrubbed_params) - - response = None - try: - response = self.handler(request) - - self._extract_custom_headers(scope.span, response.headers) - - tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, response.headers) - response.headers['Server-Timing'] = "intid;desc=%s" % scope.span.context.trace_id - except HTTPException as e: - response = e - raise - except BaseException as e: - scope.span.set_tag("http.status", 500) - - # we need to explicitly populate the `message` tag with an error here - # so that it's picked up from an SDK span - scope.span.set_tag("message", str(e)) - scope.span.log_exception(e) - - logger.debug("Pyramid Instana tween", exc_info=True) - finally: - if response: - scope.span.set_tag("http.status", response.status_int) - - if 500 <= response.status_int: - if response.exception is not None: - message = str(response.exception) - scope.span.log_exception(response.exception) - else: - message = response.status - - scope.span.set_tag("message", message) - scope.span.assure_errored() - - scope.close() - - return response - - -def includeme(config): + def __call__(self, request: "Request") -> "Response": + ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) + + with tracer.start_as_current_span("http", span_context=ctx) as span: + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute("http.host", request.host) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) + span.set_attribute(SpanAttributes.HTTP_URL, request.path) + + self._extract_custom_headers(span, request.headers) + + if len(request.query_string): + scrubbed_params = strip_secrets_from_query( + request.query_string, + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + + response = None + try: + response = self.handler(request) + if request.matched_route is not None: + span.set_attribute("http.path_tpl", request.matched_route.pattern) + + self._extract_custom_headers(span, response.headers) + + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers["Server-Timing"] = ( + "intid;desc=%s" % span.context.trace_id + ) + except HTTPException as e: + response = e + raise + except BaseException as e: + span.set_attribute("http.status", 500) + + # we need to explicitly populate the `message` tag with an error here + # so that it's picked up from an SDK span + span.set_attribute("message", str(e)) + span.record_exception(e) + + logger.debug("Pyramid Instana tween", exc_info=True) + finally: + if response: + span.set_attribute("http.status", response.status_int) + + if 500 <= response.status_int: + if response.exception is not None: + message = str(response.exception) + span.record_exception(response.exception) + else: + message = response.status + + span.set_attribute("message", message) + span.assure_errored() + + return response + + +def includeme(config: "Configurator") -> None: logger.debug("Instrumenting pyramid") - config.add_tween(__name__ + '.InstanaTweenFactory') + config.add_tween(__name__ + ".InstanaTweenFactory") From d219b58e9b3b00b4223067416bf1152904ecb4e1 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Tue, 17 Sep 2024 19:57:24 +0530 Subject: [PATCH 168/172] pyramid: adapt tests after refactor (cherry picked from commit 126e4ec09440509da867e7b7aa49dfaf0c8a867d) Signed-off-by: Varsha GS --- tests/apps/pyramid_app/__init__.py | 6 +- tests/apps/pyramid_app/app.py | 44 ++- tests/conftest.py | 6 - tests/frameworks/test_pyramid.py | 578 +++++++++++++++++------------ 4 files changed, 370 insertions(+), 264 deletions(-) diff --git a/tests/apps/pyramid_app/__init__.py b/tests/apps/pyramid_app/__init__.py index 31416ae4..c42e24b4 100644 --- a/tests/apps/pyramid_app/__init__.py +++ b/tests/apps/pyramid_app/__init__.py @@ -2,10 +2,10 @@ # (c) Copyright Instana Inc. 2020 import os -from .app import pyramid_server as server -from ..utils import launch_background_thread +from tests.apps.pyramid_app.app import pyramid_server as server +from tests.apps.utils import launch_background_thread app_thread = None -if not os.environ.get('CASSANDRA_TEST'): +if not os.environ.get("CASSANDRA_TEST"): app_thread = launch_background_thread(server.serve_forever, "Pyramid") diff --git a/tests/apps/pyramid_app/app.py b/tests/apps/pyramid_app/app.py index 56dd3f15..89c00b16 100644 --- a/tests/apps/pyramid_app/app.py +++ b/tests/apps/pyramid_app/app.py @@ -8,42 +8,50 @@ from pyramid.response import Response import pyramid.httpexceptions as exc -from ...helpers import testenv +from tests.helpers import testenv logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) testenv["pyramid_port"] = 10815 -testenv["pyramid_server"] = ("http://127.0.0.1:" + str(testenv["pyramid_port"])) +testenv["pyramid_server"] = "http://127.0.0.1:" + str(testenv["pyramid_port"]) + def hello_world(request): - return Response('Ok') + return Response("Ok") + def please_fail(request): raise exc.HTTPInternalServerError("internal error") + def tableflip(request): raise BaseException("fake exception") + def response_headers(request): - headers = { - 'X-Capture-This': 'Ok', - 'X-Capture-That': 'Ok too' - } + headers = {"X-Capture-This": "Ok", "X-Capture-That": "Ok too"} return Response("Stan wuz here with headers!", headers=headers) + +def hello_user(request): + user = request.matchdict["user"] + return Response(f"Hello {user}!") + + app = None with Configurator() as config: - config.add_tween('instana.instrumentation.pyramid.tweens.InstanaTweenFactory') - config.add_route('hello', '/') - config.add_view(hello_world, route_name='hello') - config.add_route('fail', '/500') - config.add_view(please_fail, route_name='fail') - config.add_route('crash', '/exception') - config.add_view(tableflip, route_name='crash') - config.add_route('response_headers', '/response_headers') - config.add_view(response_headers, route_name='response_headers') + config.include("instana.instrumentation.pyramid.tweens") + config.add_route("hello", "/") + config.add_view(hello_world, route_name="hello") + config.add_route("fail", "/500") + config.add_view(please_fail, route_name="fail") + config.add_route("crash", "/exception") + config.add_view(tableflip, route_name="crash") + config.add_route("response_headers", "/response_headers") + config.add_view(response_headers, route_name="response_headers") + config.add_route("hello_user", "/hello_user/{user}") + config.add_view(hello_user, route_name="hello_user") app = config.make_wsgi_app() - -pyramid_server = make_server('127.0.0.1', testenv["pyramid_port"], app) +pyramid_server = make_server("127.0.0.1", testenv["pyramid_port"], app) diff --git a/tests/conftest.py b/tests/conftest.py index c7c7cb8b..3d35e568 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,7 +38,6 @@ collect_ignore_glob.append("*frameworks/test_celery*") collect_ignore_glob.append("*frameworks/test_gevent*") collect_ignore_glob.append("*frameworks/test_grpcio*") -collect_ignore_glob.append("*frameworks/test_pyramid*") collect_ignore_glob.append("*frameworks/test_tornado*") # # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will @@ -80,11 +79,6 @@ collect_ignore_glob.append("*test_pep0249*") collect_ignore_glob.append("*test_sqlalchemy*") - # Currently the latest version of pyramid depends on the `cgi` module - # which has been deprecated since Python 3.11 and finally removed in 3.13 - # `ModuleNotFoundError: No module named 'cgi'` - collect_ignore_glob.append("*test_pyramid*") - # Currently not installable dependencies because of 3.13 incompatibilities collect_ignore_glob.append("*test_fastapi*") collect_ignore_glob.append("*test_google-cloud-pubsub*") diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index f3f88fb0..d2378d3a 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -1,319 +1,337 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import unittest - +import pytest import urllib3 +from typing import Generator import tests.apps.pyramid_app -from ..helpers import testenv +from tests.helpers import testenv from instana.singletons import tracer, agent +from instana.span.span import get_current_span -class TestPyramid(unittest.TestCase): - def setUp(self): - """ Clear all spans before a test run """ +class TestPyramid: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + """Clear all spans before a test run""" self.http = urllib3.PoolManager() - self.recorder = tracer.recorder + self.recorder = tracer.span_processor self.recorder.clear_spans() - def tearDown(self): - """ Do nothing for now """ - return None - - def test_vanilla_requests(self): - r = self.http.request('GET', testenv["pyramid_server"] + '/') - self.assertEqual(r.status, 200) + def test_vanilla_requests(self) -> None: + r = self.http.request("GET", testenv["pyramid_server"] + "/") + assert r.status == 200 spans = self.recorder.queued_spans() - self.assertEqual(1, len(spans)) + assert len(spans) == 1 - def test_get_request(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/') + def test_get_request(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["pyramid_server"] + "/") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], pyramid_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(pyramid_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], pyramid_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(pyramid_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % pyramid_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Synthetic - self.assertIsNone(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(pyramid_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - - self.assertTrue(pyramid_span.data["sdk"]) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(200, sdk_custom_tags["http.status"]) - self.assertNotIn("message", sdk_custom_tags) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert "message" not in sdk_custom_attributes + assert sdk_custom_attributes["http.path_tpl"] == "/" # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - 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) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert testenv["pyramid_server"] + "/" == urllib3_span.data["http"]["url"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + def test_synthetic_request(self) -> None: + headers = {"X-INSTANA-SYNTHETIC": "1"} + + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/", headers=headers + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 - self.assertTrue(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy - def test_500(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/500') + def test_500(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request("GET", testenv["pyramid_server"] + "/500") spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert response.status == 500 - self.assertIn('X-INSTANA-T', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-T'], 16)) - self.assertEqual(response.headers['X-INSTANA-T'], pyramid_span.t) + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(pyramid_span.t) - self.assertIn('X-INSTANA-S', response.headers) - self.assertTrue(int(response.headers['X-INSTANA-S'], 16)) - self.assertEqual(response.headers['X-INSTANA-S'], pyramid_span.s) + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(pyramid_span.s) - self.assertIn('X-INSTANA-L', response.headers) - self.assertEqual(response.headers['X-INSTANA-L'], '1') + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" - self.assertIn('Server-Timing', response.headers) + assert "Server-Timing" in response.headers server_timing_value = "intid;desc=%s" % pyramid_span.t - self.assertEqual(response.headers['Server-Timing'], server_timing_value) + assert response.headers["Server-Timing"] == server_timing_value - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, pyramid_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert pyramid_span.ec == 1 # wsgi - self.assertEqual("sdk", pyramid_span.n) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/500', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(500, sdk_custom_tags["http.status"]) - self.assertEqual("internal error", sdk_custom_tags["message"]) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/500" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 500 + assert sdk_custom_attributes["message"] == "internal error" + assert sdk_custom_attributes["http.path_tpl"] == "/500" # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/500', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - def test_exception(self): - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/exception') + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 500 + assert testenv["pyramid_server"] + "/500" == urllib3_span.data["http"]["url"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + def test_exception(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/exception" + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(500, response.status) + assert response + assert response.status == 500 - self.assertIsNone(tracer.active_span) + assert not get_current_span().is_recording() # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(test_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert test_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(1, urllib3_span.ec) - self.assertEqual(1, pyramid_span.ec) + assert not test_span.ec + assert urllib3_span.ec == 1 + assert pyramid_span.ec == 1 # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/exception', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(500, sdk_custom_tags["http.status"]) - self.assertEqual("fake exception", sdk_custom_tags["message"]) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/exception" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 500 + assert sdk_custom_attributes["message"] == "fake exception" + assert "http.path_tpl" not in sdk_custom_attributes # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(500, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/exception', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - def test_response_header_capture(self): + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 500 + assert ( + testenv["pyramid_server"] + "/exception" == urllib3_span.data["http"]["url"] + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + def test_response_header_capture(self) -> None: # Hack together a manual custom headers list original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] - with tracer.start_active_span('test'): - response = self.http.request('GET', testenv["pyramid_server"] + '/response_headers') + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/response_headers" + ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Synthetic - self.assertIsNone(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(pyramid_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - - self.assertTrue(pyramid_span.data["sdk"]) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/response_headers', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(200, sdk_custom_tags["http.status"]) - self.assertNotIn("message", sdk_custom_tags) + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/response_headers" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert "message" not in sdk_custom_attributes # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/response_headers', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) - - - self.assertTrue(sdk_custom_tags["http.header.X-Capture-This"]) - self.assertEqual("Ok", sdk_custom_tags["http.header.X-Capture-This"]) - self.assertTrue(sdk_custom_tags["http.header.X-Capture-That"]) - self.assertEqual("Ok too", sdk_custom_tags["http.header.X-Capture-That"]) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert ( + testenv["pyramid_server"] + "/response_headers" + == urllib3_span.data["http"]["url"] + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 + + assert sdk_custom_attributes["http.header.X-Capture-This"] + assert sdk_custom_attributes["http.header.X-Capture-This"] == "Ok" + assert sdk_custom_attributes["http.header.X-Capture-That"] + assert sdk_custom_attributes["http.header.X-Capture-That"] == "Ok too" agent.options.extra_http_headers = original_extra_http_headers - def test_request_header_capture(self): + def test_request_header_capture(self) -> None: original_extra_http_headers = agent.options.extra_http_headers agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] @@ -322,68 +340,154 @@ def test_request_header_capture(self): "X-Capture-That-Too": "that too", } - with tracer.start_active_span("test"): + with tracer.start_as_current_span("test"): response = self.http.request( "GET", testenv["pyramid_server"] + "/", headers=request_headers ) spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) + assert len(spans) == 3 pyramid_span = spans[0] urllib3_span = spans[1] test_span = spans[2] - self.assertTrue(response) - self.assertEqual(200, response.status) + assert response + assert response.status == 200 # Same traceId - self.assertEqual(test_span.t, urllib3_span.t) - self.assertEqual(urllib3_span.t, pyramid_span.t) + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t # Parent relationships - self.assertEqual(urllib3_span.p, test_span.s) - self.assertEqual(pyramid_span.p, urllib3_span.s) + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s # Synthetic - self.assertIsNone(pyramid_span.sy) - self.assertIsNone(urllib3_span.sy) - self.assertIsNone(test_span.sy) + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(urllib3_span.ec) - self.assertIsNone(pyramid_span.ec) + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec # HTTP SDK span - self.assertEqual("sdk", pyramid_span.n) - - self.assertTrue(pyramid_span.data["sdk"]) - self.assertEqual('http', pyramid_span.data["sdk"]["name"]) - self.assertEqual('entry', pyramid_span.data["sdk"]["type"]) - - sdk_custom_tags = pyramid_span.data["sdk"]["custom"]["tags"] - self.assertEqual('127.0.0.1:' + str(testenv['pyramid_port']), sdk_custom_tags["http.host"]) - self.assertEqual('/', sdk_custom_tags["http.url"]) - self.assertEqual('GET', sdk_custom_tags["http.method"]) - self.assertEqual(200, sdk_custom_tags["http.status"]) - self.assertNotIn("message", sdk_custom_tags) - self.assertNotIn("http.path_tpl", sdk_custom_tags) + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert "message" not in sdk_custom_attributes + assert sdk_custom_attributes["http.path_tpl"] == "/" # urllib3 - self.assertEqual("test", test_span.data["sdk"]["name"]) - self.assertEqual("urllib3", urllib3_span.n) - self.assertEqual(200, urllib3_span.data["http"]["status"]) - self.assertEqual(testenv["pyramid_server"] + '/', urllib3_span.data["http"]["url"]) - self.assertEqual("GET", urllib3_span.data["http"]["method"]) - self.assertIsNotNone(urllib3_span.stack) - self.assertTrue(type(urllib3_span.stack) is list) - self.assertTrue(len(urllib3_span.stack) > 1) + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert testenv["pyramid_server"] + "/" == urllib3_span.data["http"]["url"] + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 # custom headers - self.assertTrue(sdk_custom_tags["http.header.X-Capture-This-Too"]) - self.assertEqual("this too", sdk_custom_tags["http.header.X-Capture-This-Too"]) - self.assertTrue(sdk_custom_tags["http.header.X-Capture-That-Too"]) - self.assertEqual("that too", sdk_custom_tags["http.header.X-Capture-That-Too"]) + assert sdk_custom_attributes["http.header.X-Capture-This-Too"] + assert sdk_custom_attributes["http.header.X-Capture-This-Too"] == "this too" + assert sdk_custom_attributes["http.header.X-Capture-That-Too"] + assert sdk_custom_attributes["http.header.X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers + + def test_scrub_secret_path_template(self) -> None: + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["pyramid_server"] + "/hello_user/oswald?secret=sshhh" + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + pyramid_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + assert response + assert response.status == 200 + + assert "X-INSTANA-T" in response.headers + assert int(response.headers["X-INSTANA-T"], 16) + assert response.headers["X-INSTANA-T"] == str(pyramid_span.t) + + assert "X-INSTANA-S" in response.headers + assert int(response.headers["X-INSTANA-S"], 16) + assert response.headers["X-INSTANA-S"] == str(pyramid_span.s) + + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + + assert "Server-Timing" in response.headers + server_timing_value = "intid;desc=%s" % pyramid_span.t + assert response.headers["Server-Timing"] == server_timing_value + + assert not get_current_span().is_recording() + + # Same traceId + assert test_span.t == urllib3_span.t + assert urllib3_span.t == pyramid_span.t + + # Parent relationships + assert urllib3_span.p == test_span.s + assert pyramid_span.p == urllib3_span.s + + # Synthetic + assert not pyramid_span.sy + assert not urllib3_span.sy + assert not test_span.sy + + # Error logging + assert not test_span.ec + assert not urllib3_span.ec + assert not pyramid_span.ec + + # HTTP SDK span + assert pyramid_span.n == "sdk" + + assert pyramid_span.data["sdk"] + assert pyramid_span.data["sdk"]["name"] == "http" + assert pyramid_span.data["sdk"]["type"] == "entry" + + sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] + assert ( + "127.0.0.1:" + str(testenv["pyramid_port"]) + == sdk_custom_attributes["http.host"] + ) + assert sdk_custom_attributes["http.url"] == "/hello_user/oswald" + assert sdk_custom_attributes["http.method"] == "GET" + assert sdk_custom_attributes["http.status"] == 200 + assert sdk_custom_attributes["http.params"] == "secret=" + assert "message" not in sdk_custom_attributes + assert sdk_custom_attributes["http.path_tpl"] == "/hello_user/{user}" + + # urllib3 + assert test_span.data["sdk"]["name"] == "test" + assert urllib3_span.n == "urllib3" + assert urllib3_span.data["http"]["status"] == 200 + assert ( + testenv["pyramid_server"] + sdk_custom_attributes["http.url"] + == urllib3_span.data["http"]["url"] + ) + assert urllib3_span.data["http"]["method"] == "GET" + assert urllib3_span.stack + assert type(urllib3_span.stack) is list + assert len(urllib3_span.stack) > 1 From 480ff099f649f11d039416848aa80da73b1795b7 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Fri, 20 Sep 2024 15:06:04 +0530 Subject: [PATCH 169/172] pyramid: enable auto-instrumentation Signed-off-by: Varsha GS --- src/instana/__init__.py | 1 + .../instrumentation/pyramid/__init__.py | 0 src/instana/instrumentation/pyramid/tweens.py | 109 ------------- src/instana/instrumentation/pyramid_inst.py | 147 ++++++++++++++++++ .../{ => pyramid}/pyramid_app/__init__.py | 2 +- tests/apps/{ => pyramid}/pyramid_app/app.py | 6 +- tests/apps/pyramid/pyramid_utils/tweens.py | 16 ++ tests/frameworks/test_pyramid.py | 2 +- 8 files changed, 170 insertions(+), 113 deletions(-) delete mode 100644 src/instana/instrumentation/pyramid/__init__.py delete mode 100644 src/instana/instrumentation/pyramid/tweens.py create mode 100644 src/instana/instrumentation/pyramid_inst.py rename tests/apps/{ => pyramid}/pyramid_app/__init__.py (78%) rename tests/apps/{ => pyramid}/pyramid_app/app.py (91%) create mode 100644 tests/apps/pyramid/pyramid_utils/tweens.py diff --git a/src/instana/__init__.py b/src/instana/__init__.py index f81e5815..5e4e610f 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -183,6 +183,7 @@ def boot_agent(): starlette_inst, # noqa: F401 sanic_inst, # noqa: F401 urllib3, # noqa: F401 + pyramid_inst, ) from instana.instrumentation.aiohttp import ( client, # noqa: F401 diff --git a/src/instana/instrumentation/pyramid/__init__.py b/src/instana/instrumentation/pyramid/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/instana/instrumentation/pyramid/tweens.py b/src/instana/instrumentation/pyramid/tweens.py deleted file mode 100644 index aed34e63..00000000 --- a/src/instana/instrumentation/pyramid/tweens.py +++ /dev/null @@ -1,109 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - - -from pyramid.httpexceptions import HTTPException -from typing import TYPE_CHECKING, Dict, Any, Callable - -from opentelemetry.semconv.trace import SpanAttributes -from opentelemetry.trace import SpanKind - -from instana.log import logger -from instana.singletons import tracer, agent -from instana.util.secrets import strip_secrets_from_query -from instana.propagators.format import Format - -if TYPE_CHECKING: - from pyramid.request import Request - from pyramid.response import Response - from pyramid.config import Configurator - from instana.span.span import InstanaSpan - from pyramid.registry import Registry - - -class InstanaTweenFactory(object): - """A factory that provides Instana instrumentation tween for Pyramid apps""" - - def __init__( - self, handler: Callable[["Request"], "Response"], registry: "Registry" - ) -> None: - self.handler = handler - - def _extract_custom_headers( - self, span: "InstanaSpan", headers: Dict[str, Any] - ) -> None: - if not agent.options.extra_http_headers: - return - try: - for custom_header in agent.options.extra_http_headers: - if custom_header in headers: - span.set_attribute( - "http.header.%s" % custom_header, headers[custom_header] - ) - - except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) - - def __call__(self, request: "Request") -> "Response": - ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) - - with tracer.start_as_current_span("http", span_context=ctx) as span: - span.set_attribute("span.kind", SpanKind.SERVER) - span.set_attribute("http.host", request.host) - span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) - span.set_attribute(SpanAttributes.HTTP_URL, request.path) - - self._extract_custom_headers(span, request.headers) - - if len(request.query_string): - scrubbed_params = strip_secrets_from_query( - request.query_string, - agent.options.secrets_matcher, - agent.options.secrets_list, - ) - span.set_attribute("http.params", scrubbed_params) - - response = None - try: - response = self.handler(request) - if request.matched_route is not None: - span.set_attribute("http.path_tpl", request.matched_route.pattern) - - self._extract_custom_headers(span, response.headers) - - tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) - response.headers["Server-Timing"] = ( - "intid;desc=%s" % span.context.trace_id - ) - except HTTPException as e: - response = e - raise - except BaseException as e: - span.set_attribute("http.status", 500) - - # we need to explicitly populate the `message` tag with an error here - # so that it's picked up from an SDK span - span.set_attribute("message", str(e)) - span.record_exception(e) - - logger.debug("Pyramid Instana tween", exc_info=True) - finally: - if response: - span.set_attribute("http.status", response.status_int) - - if 500 <= response.status_int: - if response.exception is not None: - message = str(response.exception) - span.record_exception(response.exception) - else: - message = response.status - - span.set_attribute("message", message) - span.assure_errored() - - return response - - -def includeme(config: "Configurator") -> None: - logger.debug("Instrumenting pyramid") - config.add_tween(__name__ + ".InstanaTweenFactory") diff --git a/src/instana/instrumentation/pyramid_inst.py b/src/instana/instrumentation/pyramid_inst.py new file mode 100644 index 00000000..7527aa6f --- /dev/null +++ b/src/instana/instrumentation/pyramid_inst.py @@ -0,0 +1,147 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +try: + from pyramid.httpexceptions import HTTPException + from pyramid.path import caller_package + from pyramid.settings import aslist + from pyramid.tweens import EXCVIEW + from pyramid.config import Configurator + from typing import TYPE_CHECKING, Dict, Any, Callable, Tuple + import wrapt + + from opentelemetry.semconv.trace import SpanAttributes + from opentelemetry.trace import SpanKind + + from instana.log import logger + from instana.singletons import tracer, agent + from instana.util.secrets import strip_secrets_from_query + from instana.propagators.format import Format + + if TYPE_CHECKING: + from pyramid.request import Request + from pyramid.response import Response + from instana.span.span import InstanaSpan + from pyramid.registry import Registry + + class InstanaTweenFactory(object): + """A factory that provides Instana instrumentation tween for Pyramid apps""" + + def __init__( + self, handler: Callable[["Request"], "Response"], registry: "Registry" + ) -> None: + self.handler = handler + + def _extract_custom_headers( + self, span: "InstanaSpan", headers: Dict[str, Any] + ) -> None: + if not agent.options.extra_http_headers: + return + try: + for custom_header in agent.options.extra_http_headers: + if custom_header in headers: + span.set_attribute( + "http.header.%s" % custom_header, headers[custom_header] + ) + + except Exception: + logger.debug("extract_custom_headers: ", exc_info=True) + + def __call__(self, request: "Request") -> "Response": + ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) + + with tracer.start_as_current_span("http", span_context=ctx) as span: + span.set_attribute("span.kind", SpanKind.SERVER) + span.set_attribute("http.host", request.host) + span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) + span.set_attribute(SpanAttributes.HTTP_URL, request.path) + + self._extract_custom_headers(span, request.headers) + + if len(request.query_string): + scrubbed_params = strip_secrets_from_query( + request.query_string, + agent.options.secrets_matcher, + agent.options.secrets_list, + ) + span.set_attribute("http.params", scrubbed_params) + + response = None + try: + response = self.handler(request) + if request.matched_route is not None: + span.set_attribute( + "http.path_tpl", request.matched_route.pattern + ) + + self._extract_custom_headers(span, response.headers) + + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) + response.headers["Server-Timing"] = ( + "intid;desc=%s" % span.context.trace_id + ) + except HTTPException as e: + response = e + raise + except BaseException as e: + span.set_attribute("http.status", 500) + + # we need to explicitly populate the `message` tag with an error here + # so that it's picked up from an SDK span + span.set_attribute("message", str(e)) + span.record_exception(e) + + logger.debug("Pyramid Instana tween", exc_info=True) + finally: + if response: + span.set_attribute("http.status", response.status_int) + + if 500 <= response.status_int: + if response.exception is not None: + message = str(response.exception) + span.record_exception(response.exception) + else: + message = response.status + + span.set_attribute("message", message) + span.assure_errored() + + return response + + INSTANA_TWEEN = __name__ + ".InstanaTweenFactory" + + # implicit tween ordering + def includeme(config: Configurator) -> None: + logger.debug("Instrumenting pyramid") + config.add_tween(INSTANA_TWEEN) + + # explicit tween ordering + @wrapt.patch_function_wrapper("pyramid.config", "Configurator.__init__") + def init_with_instana( + wrapped: Callable[..., Configurator.__init__], + instance: Configurator, + args: Tuple[object, ...], + kwargs: Dict[str, Any], + ): + settings = kwargs.get("settings", {}) + tweens = aslist(settings.get("pyramid.tweens", [])) + + if tweens and INSTANA_TWEEN not in settings: + # pyramid.tweens.EXCVIEW is the name of built-in exception view provided by + # pyramid. We need our tween to be before it, otherwise unhandled + # exceptions will be caught before they reach our tween. + if EXCVIEW in tweens: + tweens = [INSTANA_TWEEN] + tweens + else: + tweens = [INSTANA_TWEEN] + tweens + [EXCVIEW] + settings["pyramid.tweens"] = "\n".join(tweens) + kwargs["settings"] = settings + + if not kwargs.get("package", None): + kwargs["package"] = caller_package() + + wrapped(*args, **kwargs) + instance.include(__name__) + +except ImportError: + pass diff --git a/tests/apps/pyramid_app/__init__.py b/tests/apps/pyramid/pyramid_app/__init__.py similarity index 78% rename from tests/apps/pyramid_app/__init__.py rename to tests/apps/pyramid/pyramid_app/__init__.py index c42e24b4..bae66790 100644 --- a/tests/apps/pyramid_app/__init__.py +++ b/tests/apps/pyramid/pyramid_app/__init__.py @@ -2,7 +2,7 @@ # (c) Copyright Instana Inc. 2020 import os -from tests.apps.pyramid_app.app import pyramid_server as server +from tests.apps.pyramid.pyramid_app.app import pyramid_server as server from tests.apps.utils import launch_background_thread app_thread = None diff --git a/tests/apps/pyramid_app/app.py b/tests/apps/pyramid/pyramid_app/app.py similarity index 91% rename from tests/apps/pyramid_app/app.py rename to tests/apps/pyramid/pyramid_app/app.py index 89c00b16..867b2e7c 100644 --- a/tests/apps/pyramid_app/app.py +++ b/tests/apps/pyramid/pyramid_app/app.py @@ -40,8 +40,10 @@ def hello_user(request): app = None -with Configurator() as config: - config.include("instana.instrumentation.pyramid.tweens") +settings = { + "pyramid.tweens": "tests.apps.pyramid.pyramid_utils.tweens.timing_tween_factory", +} +with Configurator(settings=settings) as config: config.add_route("hello", "/") config.add_view(hello_world, route_name="hello") config.add_route("fail", "/500") diff --git a/tests/apps/pyramid/pyramid_utils/tweens.py b/tests/apps/pyramid/pyramid_utils/tweens.py new file mode 100644 index 00000000..0183df4b --- /dev/null +++ b/tests/apps/pyramid/pyramid_utils/tweens.py @@ -0,0 +1,16 @@ +# (c) Copyright IBM Corp. 2024 + +import time + + +def timing_tween_factory(handler, registry): + def timing_tween(request): + start = time.time() + try: + response = handler(request) + finally: + end = time.time() + print(f"The request took {end - start} seconds") + return response + + return timing_tween diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index d2378d3a..dcd41474 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -5,7 +5,7 @@ import urllib3 from typing import Generator -import tests.apps.pyramid_app +import tests.apps.pyramid.pyramid_app from tests.helpers import testenv from instana.singletons import tracer, agent from instana.span.span import get_current_span From 863cf9b32f579cab4531e72c0b45da462d8ee984 Mon Sep 17 00:00:00 2001 From: Varsha GS Date: Sat, 21 Sep 2024 10:52:00 +0530 Subject: [PATCH 170/172] pyramid: change from sdk span (http) to registered entry span (wsgi) Signed-off-by: Varsha GS --- src/instana/__init__.py | 2 +- .../{pyramid_inst.py => pyramid.py} | 31 ++-- tests/frameworks/test_pyramid.py | 169 ++++++------------ 3 files changed, 73 insertions(+), 129 deletions(-) rename src/instana/instrumentation/{pyramid_inst.py => pyramid.py} (83%) diff --git a/src/instana/__init__.py b/src/instana/__init__.py index 5e4e610f..bb4d53b9 100644 --- a/src/instana/__init__.py +++ b/src/instana/__init__.py @@ -178,12 +178,12 @@ def boot_agent(): psycopg2, # noqa: F401 pymongo, # noqa: F401 pymysql, # noqa: F401 + pyramid, # noqa: F401 redis, # noqa: F401 sqlalchemy, # noqa: F401 starlette_inst, # noqa: F401 sanic_inst, # noqa: F401 urllib3, # noqa: F401 - pyramid_inst, ) from instana.instrumentation.aiohttp import ( client, # noqa: F401 diff --git a/src/instana/instrumentation/pyramid_inst.py b/src/instana/instrumentation/pyramid.py similarity index 83% rename from src/instana/instrumentation/pyramid_inst.py rename to src/instana/instrumentation/pyramid.py index 7527aa6f..85fd1829 100644 --- a/src/instana/instrumentation/pyramid_inst.py +++ b/src/instana/instrumentation/pyramid.py @@ -41,7 +41,7 @@ def _extract_custom_headers( for custom_header in agent.options.extra_http_headers: if custom_header in headers: span.set_attribute( - "http.header.%s" % custom_header, headers[custom_header] + f"http.header.{custom_header}", headers[custom_header] ) except Exception: @@ -50,7 +50,7 @@ def _extract_custom_headers( def __call__(self, request: "Request") -> "Response": ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) - with tracer.start_as_current_span("http", span_context=ctx) as span: + with tracer.start_as_current_span("wsgi", span_context=ctx) as span: span.set_attribute("span.kind", SpanKind.SERVER) span.set_attribute("http.host", request.host) span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) @@ -78,32 +78,29 @@ def __call__(self, request: "Request") -> "Response": tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) response.headers["Server-Timing"] = ( - "intid;desc=%s" % span.context.trace_id + f"intid;desc={span.context.trace_id}" ) except HTTPException as e: response = e - raise + logger.debug( + "Pyramid InstanaTweenFactory HTTPException: ", exc_info=True + ) except BaseException as e: - span.set_attribute("http.status", 500) - - # we need to explicitly populate the `message` tag with an error here - # so that it's picked up from an SDK span - span.set_attribute("message", str(e)) + span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, 500) span.record_exception(e) - logger.debug("Pyramid Instana tween", exc_info=True) + logger.debug( + "Pyramid InstanaTweenFactory BaseException: ", exc_info=True + ) finally: if response: - span.set_attribute("http.status", response.status_int) + span.set_attribute( + SpanAttributes.HTTP_STATUS_CODE, response.status_int + ) if 500 <= response.status_int: - if response.exception is not None: - message = str(response.exception) + if response.exception: span.record_exception(response.exception) - else: - message = response.status - - span.set_attribute("message", message) span.assure_errored() return response diff --git a/tests/frameworks/test_pyramid.py b/tests/frameworks/test_pyramid.py index dcd41474..6aa39ca4 100644 --- a/tests/frameworks/test_pyramid.py +++ b/tests/frameworks/test_pyramid.py @@ -37,7 +37,6 @@ def test_get_request(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 200 assert "X-INSTANA-T" in response.headers @@ -75,23 +74,14 @@ def test_get_request(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert "message" not in sdk_custom_attributes - assert sdk_custom_attributes["http.path_tpl"] == "/" + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -118,7 +108,6 @@ def test_synthetic_request(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 200 assert pyramid_span.sy @@ -137,7 +126,6 @@ def test_500(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 500 assert "X-INSTANA-T" in response.headers @@ -171,20 +159,13 @@ def test_500(self) -> None: assert pyramid_span.ec == 1 # wsgi - assert pyramid_span.n == "sdk" - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/500" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 500 - assert sdk_custom_attributes["message"] == "internal error" - assert sdk_custom_attributes["http.path_tpl"] == "/500" + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/500" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 500 + assert pyramid_span.data["http"]["error"] == "internal error" + assert pyramid_span.data["http"]["path_tpl"] == "/500" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -210,7 +191,6 @@ def test_exception(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 500 assert not get_current_span().is_recording() @@ -228,21 +208,14 @@ def test_exception(self) -> None: assert urllib3_span.ec == 1 assert pyramid_span.ec == 1 - # HTTP SDK span - assert pyramid_span.n == "sdk" - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/exception" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 500 - assert sdk_custom_attributes["message"] == "fake exception" - assert "http.path_tpl" not in sdk_custom_attributes + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/exception" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 500 + assert pyramid_span.data["http"]["error"] == "fake exception" + assert not pyramid_span.data["http"]["path_tpl"] # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -294,22 +267,14 @@ def test_response_header_capture(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/response_headers" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert "message" not in sdk_custom_attributes + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/response_headers" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/response_headers" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -324,10 +289,11 @@ def test_response_header_capture(self) -> None: assert type(urllib3_span.stack) is list assert len(urllib3_span.stack) > 1 - assert sdk_custom_attributes["http.header.X-Capture-This"] - assert sdk_custom_attributes["http.header.X-Capture-This"] == "Ok" - assert sdk_custom_attributes["http.header.X-Capture-That"] - assert sdk_custom_attributes["http.header.X-Capture-That"] == "Ok too" + # custom headers + assert "X-Capture-This" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-This"] == "Ok" + assert "X-Capture-That" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-That"] == "Ok too" agent.options.extra_http_headers = original_extra_http_headers @@ -352,7 +318,6 @@ def test_request_header_capture(self) -> None: urllib3_span = spans[1] test_span = spans[2] - assert response assert response.status == 200 # Same traceId @@ -373,23 +338,14 @@ def test_request_header_capture(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert "message" not in sdk_custom_attributes - assert sdk_custom_attributes["http.path_tpl"] == "/" + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/" # urllib3 assert test_span.data["sdk"]["name"] == "test" @@ -402,10 +358,10 @@ def test_request_header_capture(self) -> None: assert len(urllib3_span.stack) > 1 # custom headers - assert sdk_custom_attributes["http.header.X-Capture-This-Too"] - assert sdk_custom_attributes["http.header.X-Capture-This-Too"] == "this too" - assert sdk_custom_attributes["http.header.X-Capture-That-Too"] - assert sdk_custom_attributes["http.header.X-Capture-That-Too"] == "that too" + assert "X-Capture-This-Too" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in pyramid_span.data["http"]["header"] + assert pyramid_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" agent.options.extra_http_headers = original_extra_http_headers @@ -460,31 +416,22 @@ def test_scrub_secret_path_template(self) -> None: assert not urllib3_span.ec assert not pyramid_span.ec - # HTTP SDK span - assert pyramid_span.n == "sdk" - - assert pyramid_span.data["sdk"] - assert pyramid_span.data["sdk"]["name"] == "http" - assert pyramid_span.data["sdk"]["type"] == "entry" - - sdk_custom_attributes = pyramid_span.data["sdk"]["custom"]["attributes"] - assert ( - "127.0.0.1:" + str(testenv["pyramid_port"]) - == sdk_custom_attributes["http.host"] - ) - assert sdk_custom_attributes["http.url"] == "/hello_user/oswald" - assert sdk_custom_attributes["http.method"] == "GET" - assert sdk_custom_attributes["http.status"] == 200 - assert sdk_custom_attributes["http.params"] == "secret=" - assert "message" not in sdk_custom_attributes - assert sdk_custom_attributes["http.path_tpl"] == "/hello_user/{user}" + # wsgi + assert pyramid_span.n == "wsgi" + assert pyramid_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["pyramid_port"]) + assert pyramid_span.data["http"]["url"] == "/hello_user/oswald" + assert pyramid_span.data["http"]["method"] == "GET" + assert pyramid_span.data["http"]["status"] == 200 + assert pyramid_span.data["http"]["params"] == "secret=" + assert not pyramid_span.data["http"]["error"] + assert pyramid_span.data["http"]["path_tpl"] == "/hello_user/{user}" # urllib3 assert test_span.data["sdk"]["name"] == "test" assert urllib3_span.n == "urllib3" assert urllib3_span.data["http"]["status"] == 200 assert ( - testenv["pyramid_server"] + sdk_custom_attributes["http.url"] + testenv["pyramid_server"] + pyramid_span.data["http"]["url"] == urllib3_span.data["http"]["url"] ) assert urllib3_span.data["http"]["method"] == "GET" From 64e1e0b898dd6bc6e8cd9db504cb1f4a949aefa1 Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 18 Sep 2024 12:31:25 +0300 Subject: [PATCH 171/172] unittest: updated test folder structure --- src/instana/span.py | 806 ++++++++++++++++++ tests/agents/test_host.py | 595 +++++++++++++ tests/{platforms => collector}/conftest.py | 0 tests/collector/test_base_collector.py | 200 +++++ .../test_eksfargate_collector.py | 0 tests/collector/test_host_collector.py | 303 +++++++ tests/collector/test_runtime.py | 65 ++ tests/collector/test_utils.py | 34 + tests/platforms/__init__.py | 0 tests/platforms/test_eksfargate.py | 120 --- tests/platforms/test_fargate.py | 125 --- tests/platforms/test_fargate_collector.py | 242 ------ tests/platforms/test_gcr_collector.py | 95 --- tests/platforms/test_google_cloud_run.py | 129 --- tests/platforms/test_host.py | 273 ------ tests/platforms/test_host_collector.py | 281 ------ tests/platforms/test_lambda.py | 762 ----------------- tests/propagators/test_base_propagator.py | 93 ++ tests/test_sampling.py | 20 + tests/util/test_traceutils.py | 65 ++ 20 files changed, 2181 insertions(+), 2027 deletions(-) create mode 100644 src/instana/span.py create mode 100644 tests/agents/test_host.py rename tests/{platforms => collector}/conftest.py (100%) create mode 100644 tests/collector/test_base_collector.py rename tests/{platforms => collector}/test_eksfargate_collector.py (100%) create mode 100644 tests/collector/test_host_collector.py create mode 100644 tests/collector/test_runtime.py create mode 100644 tests/collector/test_utils.py delete mode 100644 tests/platforms/__init__.py delete mode 100644 tests/platforms/test_eksfargate.py delete mode 100644 tests/platforms/test_fargate.py delete mode 100644 tests/platforms/test_fargate_collector.py delete mode 100644 tests/platforms/test_gcr_collector.py delete mode 100644 tests/platforms/test_google_cloud_run.py delete mode 100644 tests/platforms/test_host.py delete mode 100644 tests/platforms/test_host_collector.py delete mode 100644 tests/platforms/test_lambda.py create mode 100644 tests/propagators/test_base_propagator.py create mode 100644 tests/test_sampling.py create mode 100644 tests/util/test_traceutils.py diff --git a/src/instana/span.py b/src/instana/span.py new file mode 100644 index 00000000..ca268748 --- /dev/null +++ b/src/instana/span.py @@ -0,0 +1,806 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2017 + +""" +This module contains the classes that represents spans. + +InstanaSpan - the OpenTelemetry based span used during tracing + +When an InstanaSpan is finished, it is converted into either an SDKSpan +or RegisteredSpan depending on type. + +BaseSpan: Base class containing the commonalities for the two descendants + - SDKSpan: Class that represents an SDK type span + - RegisteredSpan: Class that represents a Registered type span +""" +import six +from typing import Dict, Optional, Union, Sequence, Tuple +from threading import Lock +from time import time_ns + +from opentelemetry.trace import Span # , SpanContext +from opentelemetry.util import types +from opentelemetry.trace.status import Status, StatusCode + +from .span_context import SpanContext +from .log import logger +from .util import DictionaryOfStan + + +class Event: + def __init__( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + self._name = name + self._attributes = attributes + if timestamp is None: + self._timestamp = time_ns() + else: + self._timestamp = timestamp + + @property + def name(self) -> str: + return self._name + + @property + def timestamp(self) -> int: + return self._timestamp + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + +class InstanaSpan(Span): + stack = None + synthetic = False + + def __init__( + self, + name: str, + context: SpanContext, + parent_id: Optional[str] = None, + start_time: Optional[int] = None, + end_time: Optional[int] = None, + attributes: types.Attributes = {}, + events: Sequence[Event] = [], + status: Optional[Status] = Status(StatusCode.UNSET), + ) -> None: + self._name = name + self._context = context + self._lock = Lock() + self._start_time = start_time or time_ns() + self._end_time = end_time + self._duration = 0 + self._attributes = attributes + self._events = events + self._parent_id = parent_id + self._status = status + + if context.synthetic: + self.synthetic = True + + + @property + def name(self) -> str: + return self._name + + def get_span_context(self) -> SpanContext: + return self._context + + @property + def context(self) -> SpanContext: + return self._context + + @property + def start_time(self) -> Optional[int]: + return self._start_time + + @property + def end_time(self) -> Optional[int]: + return self._end_time + + @property + def duration(self) -> int: + return self._duration + + @property + def attributes(self) -> types.Attributes: + return self._attributes + + def set_attributes(self, attributes: Dict[str, types.AttributeValue]) -> None: + if not self._attributes: + self._attributes = {} + + with self._lock: + for key, value in attributes.items(): + self._attributes[key] = value + + def set_attribute(self, key: str, value: types.AttributeValue) -> None: + return self.set_attributes({key: value}) + + @property + def events(self) -> Sequence[Event]: + return self._events + + @property + def status(self) -> Status: + return self._status + + @property + def parent_id(self) -> int: + return self._parent_id + + def update_name(self, name: str) -> None: + with self._lock: + self._name = name + + def is_recording(self) -> bool: + return self._end_time is None + + def set_status( + self, + status: Union[Status, StatusCode], + description: Optional[str] = None, + ) -> None: + # Ignore future calls if status is already set to OK + # Ignore calls to set to StatusCode.UNSET + if isinstance(status, Status): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status.status_code is StatusCode.UNSET + ): + return + if description is not None: + logger.warning( + "Description %s ignored. Use either `Status` or `(StatusCode, Description)`", + description, + ) + self._status = status + elif isinstance(status, StatusCode): + if ( + self._status + and self._status.status_code is StatusCode.OK + or status is StatusCode.UNSET + ): + return + self._status = Status(status, description) + + def add_event( + self, + name: str, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + ) -> None: + + event = Event( + name=name, + attributes=attributes, + timestamp=timestamp, + ) + + self._events.append(event) + + def record_exception( + self, + exception: Exception, + attributes: types.Attributes = None, + timestamp: Optional[int] = None, + escaped: bool = False, + ) -> None: + """ + Records an exception as a span event. This will record pertinent info from the exception and + assure that this span is marked as errored. + """ + try: + message = "" + self.mark_as_errored() + if hasattr(exception, "__str__") and len(str(exception)) > 0: + message = str(exception) + elif hasattr(exception, "message") and exception.message is not None: + message = exception.message + else: + message = repr(exception) + + if self.name in ["rpc-server", "rpc-client"]: + self.set_attribute("rpc.error", message) + elif self.name == "mysql": + self.set_attribute("mysql.error", message) + elif self.name == "postgres": + self.set_attribute("pg.error", message) + elif self.name in RegisteredSpan.HTTP_SPANS: + self.set_attribute("http.error", message) + elif self.name in ["celery-client", "celery-worker"]: + self.set_attribute("error", message) + elif self.name == "sqlalchemy": + self.set_attribute("sqlalchemy.err", message) + elif self.name == "aws.lambda.entry": + self.set_attribute("lambda.error", message) + else: + _attributes = {"message": message} + if attributes: + _attributes.update(attributes) + self.add_event( + name="exception", attributes=_attributes, timestamp=timestamp + ) + except Exception: + logger.debug("span.record_exception", exc_info=True) + raise + + def end(self, end_time: Optional[int] = None) -> None: + with self._lock: + self._end_time = end_time if end_time is not None else time_ns() + self._duration = self._end_time - self._start_time + + def mark_as_errored(self, attributes: types.Attributes = None) -> None: + """ + Mark this span as errored. + + @param attributes: optional attributes to add to the span + """ + try: + ec = self.attributes.get("ec", 0) + self.set_attribute("ec", ec + 1) + + if attributes is not None and isinstance(attributes, dict): + for key in attributes: + self.set_attribute(key, attributes[key]) + except Exception: + logger.debug("span.mark_as_errored", exc_info=True) + + def assure_errored(self) -> None: + """ + Make sure that this span is marked as errored. + @return: None + """ + try: + ec = self.attributes.get("ec", None) + if ec is None or ec == 0: + self.set_attribute("ec", 1) + except Exception: + logger.debug("span.assure_errored", exc_info=True) + + +class BaseSpan(object): + sy = None + + def __str__(self) -> str: + return "BaseSpan(%s)" % self.__dict__.__str__() + + def __repr__(self) -> str: + return self.__dict__.__str__() + + def __init__(self, span, source, service_name, **kwargs) -> None: + # pylint: disable=invalid-name + self.t = span.context.trace_id + self.p = span.parent_id + # self.p = span.context.span_id if span.context.is_remote else None + self.s = span.context.span_id + self.l = span.context.level + self.ts = round(span.start_time / 10**6) + self.d = round(span.duration / 10**6) + self.f = source + self.ec = span.attributes.pop("ec", None) + self.data = DictionaryOfStan() + self.stack = span.stack + + if span.synthetic is True: + self.sy = span.synthetic + + self.__dict__.update(kwargs) + + def _populate_extra_span_attributes(self, span) -> None: + if span.context.trace_parent: + self.tp = span.context.trace_parent + if span.context.instana_ancestor: + self.ia = span.context.instana_ancestor + if span.context.long_trace_id: + self.lt = span.context.long_trace_id + if span.context.correlation_type: + self.crtp = span.context.correlation_type + if span.context.correlation_id: + self.crid = span.context.correlation_id + + def _validate_attributes(self, attributes): + """ + This method will loop through a set of attributes to validate each key and value. + + :param attributes: dict of attributes + :return: dict - a filtered set of attributes + """ + filtered_attributes = DictionaryOfStan() + for key in attributes.keys(): + validated_key, validated_value = self._validate_attribute( + key, attributes[key] + ) + if validated_key is not None and validated_value is not None: + filtered_attributes[validated_key] = validated_value + return filtered_attributes + + def _validate_attribute(self, key, value): + """ + This method will assure that and are valid to set as a attribute. + If fails the check, an attempt will be made to convert it into + something useful. + + On check failure, this method will return None values indicating that the attribute is + not valid and could not be converted into something useful + + :param key: The attribute key + :param value: The attribute value + :return: Tuple (key, value) + """ + validated_key = None + validated_value = None + + try: + # Attribute keys must be some type of text or string type + if isinstance(key, (six.text_type, six.string_types)): + validated_key = key[0:1024] # Max key length of 1024 characters + + if isinstance( + value, + (bool, float, int, list, dict, six.text_type, six.string_types), + ): + validated_value = value + else: + validated_value = self._convert_attribute_value(value) + else: + logger.debug( + "(non-fatal) attribute names must be strings. attribute discarded for %s", + type(key), + ) + except Exception: + logger.debug("instana.span._validate_attribute: ", exc_info=True) + + return (validated_key, validated_value) + + def _convert_attribute_value(self, value): + final_value = None + + try: + final_value = repr(value) + except Exception: + final_value = ( + "(non-fatal) span.set_attribute: values must be one of these types: bool, float, int, list, " + "set, str or alternatively support 'repr'. attribute discarded" + ) + logger.debug(final_value, exc_info=True) + return None + return final_value + + +class SDKSpan(BaseSpan): + ENTRY_KIND = ["entry", "server", "consumer"] + EXIT_KIND = ["exit", "client", "producer"] + + def __init__(self, span, source, service_name, **kwargs) -> None: + # pylint: disable=invalid-name + super(SDKSpan, self).__init__(span, source, service_name, **kwargs) + + span_kind = self.get_span_kind(span) + + self.n = "sdk" + self.k = span_kind[1] + + if service_name is not None: + self.data["service"] = service_name + + self.data["sdk"]["name"] = span.name + self.data["sdk"]["type"] = span_kind[0] + self.data["sdk"]["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) + + if span.events is not None and len(span.events) > 0: + events = DictionaryOfStan() + for event in span.events: + filtered_attributes = self._validate_attributes(event.attributes) + if len(filtered_attributes.keys()) > 0: + events[repr(event.timestamp)] = filtered_attributes + self.data["sdk"]["custom"]["events"] = events + + if "arguments" in span.attributes: + self.data["sdk"]["arguments"] = span.attributes["arguments"] + + if "return" in span.attributes: + self.data["sdk"]["return"] = span.attributes["return"] + + # if len(span.context.baggage) > 0: + # self.data["baggage"] = span.context.baggage + + def get_span_kind(self, span) -> Tuple[str, int]: + """ + Will retrieve the `span.kind` attribute and return a tuple containing the appropriate string and integer + values for the Instana backend + + :param span: The span to search for the `span.kind` attribute + :return: Tuple (String, Int) + """ + kind = ("intermediate", 3) + if "span.kind" in span.attributes: + if span.attributes["span.kind"] in self.ENTRY_KIND: + kind = ("entry", 1) + elif span.attributes["span.kind"] in self.EXIT_KIND: + kind = ("exit", 2) + return kind + + +class RegisteredSpan(BaseSpan): + HTTP_SPANS = ( + "aiohttp-client", + "aiohttp-server", + "django", + "http", + "tornado-client", + "tornado-server", + "urllib3", + "wsgi", + "asgi", + ) + + EXIT_SPANS = ( + "aiohttp-client", + "boto3", + "cassandra", + "celery-client", + "couchbase", + "log", + "memcache", + "mongo", + "mysql", + "postgres", + "rabbitmq", + "redis", + "rpc-client", + "sqlalchemy", + "tornado-client", + "urllib3", + "pymongo", + "gcs", + "gcps-producer", + ) + + ENTRY_SPANS = ( + "aiohttp-server", + "aws.lambda.entry", + "celery-worker", + "django", + "wsgi", + "rabbitmq", + "rpc-server", + "tornado-server", + "gcps-consumer", + "asgi", + ) + + LOCAL_SPANS = "render" + + def __init__(self, span, source, service_name, **kwargs) -> None: + # pylint: disable=invalid-name + super(RegisteredSpan, self).__init__(span, source, service_name, **kwargs) + self.n = span.name + self.k = 1 + + self.data["service"] = service_name + if span.name in self.ENTRY_SPANS: + # entry + self._populate_entry_span_data(span) + self._populate_extra_span_attributes(span) + elif span.name in self.EXIT_SPANS: + self.k = 2 # exit + self._populate_exit_span_data(span) + elif span.name in self.LOCAL_SPANS: + self.k = 3 # intermediate span + self._populate_local_span_data(span) + + if "rabbitmq" in self.data and self.data["rabbitmq"]["sort"] == "publish": + self.k = 2 # exit + + # unify the span name for gcps-producer and gcps-consumer + if "gcps" in span.name: + self.n = "gcps" + + # Store any leftover attributes in the custom section + if len(span.attributes) > 0: + self.data["custom"]["attributes"] = self._validate_attributes( + span.attributes + ) + + def _populate_entry_span_data(self, span) -> None: + if span.name in self.HTTP_SPANS: + self._collect_http_attributes(span) + + elif span.name == "aws.lambda.entry": + self.data["lambda"]["arn"] = span.attributes.pop("lambda.arn", "Unknown") + self.data["lambda"]["alias"] = None + self.data["lambda"]["runtime"] = "python" + self.data["lambda"]["functionName"] = span.attributes.pop( + "lambda.name", "Unknown" + ) + self.data["lambda"]["functionVersion"] = span.attributes.pop( + "lambda.version", "Unknown" + ) + self.data["lambda"]["trigger"] = span.attributes.pop("lambda.trigger", None) + self.data["lambda"]["error"] = span.attributes.pop("lambda.error", None) + + trigger_type = self.data["lambda"]["trigger"] + + if trigger_type in ["aws:api.gateway", "aws:application.load.balancer"]: + self._collect_http_attributes(span) + elif trigger_type == "aws:cloudwatch.events": + self.data["lambda"]["cw"]["events"]["id"] = span.attributes.pop( + "data.lambda.cw.events.id", None + ) + self.data["lambda"]["cw"]["events"]["more"] = span.attributes.pop( + "lambda.cw.events.more", False + ) + self.data["lambda"]["cw"]["events"]["resources"] = span.attributes.pop( + "lambda.cw.events.resources", None + ) + + elif trigger_type == "aws:cloudwatch.logs": + self.data["lambda"]["cw"]["logs"]["group"] = span.attributes.pop( + "lambda.cw.logs.group", None + ) + self.data["lambda"]["cw"]["logs"]["stream"] = span.attributes.pop( + "lambda.cw.logs.stream", None + ) + self.data["lambda"]["cw"]["logs"]["more"] = span.attributes.pop( + "lambda.cw.logs.more", None + ) + self.data["lambda"]["cw"]["logs"]["events"] = span.attributes.pop( + "lambda.cw.logs.events", None + ) + + elif trigger_type == "aws:s3": + self.data["lambda"]["s3"]["events"] = span.attributes.pop( + "lambda.s3.events", None + ) + elif trigger_type == "aws:sqs": + self.data["lambda"]["sqs"]["messages"] = span.attributes.pop( + "lambda.sqs.messages", None + ) + + elif span.name == "celery-worker": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["retry-reason"] = span.attributes.pop( + "retry-reason", None + ) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcps-consumer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["sub"] = span.attributes.pop("gcps.sub", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "rpc-server": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + else: + logger.debug("SpanRecorder: Unknown entry span: %s" % span.name) + + def _populate_local_span_data(self, span) -> None: + if span.name == "render": + self.data["render"]["name"] = span.attributes.pop("name", None) + self.data["render"]["type"] = span.attributes.pop("type", None) + self.data["event"]["message"] = span.attributes.pop("message", None) + self.data["event"]["parameters"] = span.attributes.pop("parameters", None) + else: + logger.debug("SpanRecorder: Unknown local span: %s" % span.name) + + def _populate_exit_span_data(self, span) -> None: + if span.name in self.HTTP_SPANS: + self._collect_http_attributes(span) + + elif span.name == "boto3": + # boto3 also sends http attributes + self._collect_http_attributes(span) + + for attribute in ["op", "ep", "reg", "payload", "error"]: + value = span.attributes.pop(attribute, None) + if value is not None: + if attribute == "payload": + self.data["boto3"][attribute] = self._validate_attributes(value) + else: + self.data["boto3"][attribute] = value + + elif span.name == "cassandra": + self.data["cassandra"]["cluster"] = span.attributes.pop( + "cassandra.cluster", None + ) + self.data["cassandra"]["query"] = span.attributes.pop( + "cassandra.query", None + ) + self.data["cassandra"]["keyspace"] = span.attributes.pop( + "cassandra.keyspace", None + ) + self.data["cassandra"]["fetchSize"] = span.attributes.pop( + "cassandra.fetchSize", None + ) + self.data["cassandra"]["achievedConsistency"] = span.attributes.pop( + "cassandra.achievedConsistency", None + ) + self.data["cassandra"]["triedHosts"] = span.attributes.pop( + "cassandra.triedHosts", None + ) + self.data["cassandra"]["fullyFetched"] = span.attributes.pop( + "cassandra.fullyFetched", None + ) + self.data["cassandra"]["error"] = span.attributes.pop( + "cassandra.error", None + ) + + elif span.name == "celery-client": + self.data["celery"]["task"] = span.attributes.pop("task", None) + self.data["celery"]["task_id"] = span.attributes.pop("task_id", None) + self.data["celery"]["scheme"] = span.attributes.pop("scheme", None) + self.data["celery"]["host"] = span.attributes.pop("host", None) + self.data["celery"]["port"] = span.attributes.pop("port", None) + self.data["celery"]["error"] = span.attributes.pop("error", None) + + elif span.name == "couchbase": + self.data["couchbase"]["hostname"] = span.attributes.pop( + "couchbase.hostname", None + ) + self.data["couchbase"]["bucket"] = span.attributes.pop( + "couchbase.bucket", None + ) + self.data["couchbase"]["type"] = span.attributes.pop("couchbase.type", None) + self.data["couchbase"]["error"] = span.attributes.pop( + "couchbase.error", None + ) + self.data["couchbase"]["error_type"] = span.attributes.pop( + "couchbase.error_type", None + ) + self.data["couchbase"]["sql"] = span.attributes.pop("couchbase.sql", None) + + elif span.name == "rabbitmq": + self.data["rabbitmq"]["exchange"] = span.attributes.pop("exchange", None) + self.data["rabbitmq"]["queue"] = span.attributes.pop("queue", None) + self.data["rabbitmq"]["sort"] = span.attributes.pop("sort", None) + self.data["rabbitmq"]["address"] = span.attributes.pop("address", None) + self.data["rabbitmq"]["key"] = span.attributes.pop("key", None) + + elif span.name == "redis": + self.data["redis"]["connection"] = span.attributes.pop("connection", None) + self.data["redis"]["driver"] = span.attributes.pop("driver", None) + self.data["redis"]["command"] = span.attributes.pop("command", None) + self.data["redis"]["error"] = span.attributes.pop("redis.error", None) + self.data["redis"]["subCommands"] = span.attributes.pop("subCommands", None) + + elif span.name == "rpc-client": + self.data["rpc"]["flavor"] = span.attributes.pop("rpc.flavor", None) + self.data["rpc"]["host"] = span.attributes.pop("rpc.host", None) + self.data["rpc"]["port"] = span.attributes.pop("rpc.port", None) + self.data["rpc"]["call"] = span.attributes.pop("rpc.call", None) + self.data["rpc"]["call_type"] = span.attributes.pop("rpc.call_type", None) + self.data["rpc"]["params"] = span.attributes.pop("rpc.params", None) + # self.data["rpc"]["baggage"] = span.attributes.pop("rpc.baggage", None) + self.data["rpc"]["error"] = span.attributes.pop("rpc.error", None) + + elif span.name == "sqlalchemy": + self.data["sqlalchemy"]["sql"] = span.attributes.pop("sqlalchemy.sql", None) + self.data["sqlalchemy"]["eng"] = span.attributes.pop("sqlalchemy.eng", None) + self.data["sqlalchemy"]["url"] = span.attributes.pop("sqlalchemy.url", None) + self.data["sqlalchemy"]["err"] = span.attributes.pop("sqlalchemy.err", None) + + elif span.name == "mysql": + self.data["mysql"]["host"] = span.attributes.pop("host", None) + self.data["mysql"]["port"] = span.attributes.pop("port", None) + self.data["mysql"]["db"] = span.attributes.pop("db.instance", None) + self.data["mysql"]["user"] = span.attributes.pop("db.user", None) + self.data["mysql"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["mysql"]["error"] = span.attributes.pop("mysql.error", None) + + elif span.name == "postgres": + self.data["pg"]["host"] = span.attributes.pop("host", None) + self.data["pg"]["port"] = span.attributes.pop("port", None) + self.data["pg"]["db"] = span.attributes.pop("db.instance", None) + self.data["pg"]["user"] = span.attributes.pop("db.user", None) + self.data["pg"]["stmt"] = span.attributes.pop("db.statement", None) + self.data["pg"]["error"] = span.attributes.pop("pg.error", None) + + elif span.name == "mongo": + service = "%s:%s" % ( + span.attributes.pop("host", None), + span.attributes.pop("port", None), + ) + namespace = "%s.%s" % ( + span.attributes.pop("db", "?"), + span.attributes.pop("collection", "?"), + ) + + self.data["mongo"]["service"] = service + self.data["mongo"]["namespace"] = namespace + self.data["mongo"]["command"] = span.attributes.pop("command", None) + self.data["mongo"]["filter"] = span.attributes.pop("filter", None) + self.data["mongo"]["json"] = span.attributes.pop("json", None) + self.data["mongo"]["error"] = span.attributes.pop("error", None) + + elif span.name == "gcs": + self.data["gcs"]["op"] = span.attributes.pop("gcs.op", None) + self.data["gcs"]["bucket"] = span.attributes.pop("gcs.bucket", None) + self.data["gcs"]["object"] = span.attributes.pop("gcs.object", None) + self.data["gcs"]["entity"] = span.attributes.pop("gcs.entity", None) + self.data["gcs"]["range"] = span.attributes.pop("gcs.range", None) + self.data["gcs"]["sourceBucket"] = span.attributes.pop( + "gcs.sourceBucket", None + ) + self.data["gcs"]["sourceObject"] = span.attributes.pop( + "gcs.sourceObject", None + ) + self.data["gcs"]["sourceObjects"] = span.attributes.pop( + "gcs.sourceObjects", None + ) + self.data["gcs"]["destinationBucket"] = span.attributes.pop( + "gcs.destinationBucket", None + ) + self.data["gcs"]["destinationObject"] = span.attributes.pop( + "gcs.destinationObject", None + ) + self.data["gcs"]["numberOfOperations"] = span.attributes.pop( + "gcs.numberOfOperations", None + ) + self.data["gcs"]["projectId"] = span.attributes.pop("gcs.projectId", None) + self.data["gcs"]["accessId"] = span.attributes.pop("gcs.accessId", None) + + elif span.name == "gcps-producer": + self.data["gcps"]["op"] = span.attributes.pop("gcps.op", None) + self.data["gcps"]["projid"] = span.attributes.pop("gcps.projid", None) + self.data["gcps"]["top"] = span.attributes.pop("gcps.top", None) + + elif span.name == "log": + # use last special key values + for event in span.events: + if "message" in event.attributes: + self.data["event"]["message"] = event.attributes.pop( + "message", None + ) + if "parameters" in event.attributes: + self.data["event"]["parameters"] = event.attributes.pop( + "parameters", None + ) + else: + logger.debug("SpanRecorder: Unknown exit span: %s" % span.name) + + def _collect_http_attributes(self, span) -> None: + self.data["http"]["host"] = span.attributes.pop("http.host", None) + self.data["http"]["url"] = span.attributes.pop("http.url", None) + self.data["http"]["path"] = span.attributes.pop("http.path", None) + self.data["http"]["params"] = span.attributes.pop("http.params", None) + self.data["http"]["method"] = span.attributes.pop("http.method", None) + self.data["http"]["status"] = span.attributes.pop("http.status_code", None) + self.data["http"]["path_tpl"] = span.attributes.pop("http.path_tpl", None) + self.data["http"]["error"] = span.attributes.pop("http.error", None) + + if len(span.attributes) > 0: + custom_headers = [] + for key in span.attributes: + if key[0:12] == "http.header.": + custom_headers.append(key) + + for key in custom_headers: + trimmed_key = key[12:] + self.data["http"]["header"][trimmed_key] = span.attributes.pop(key) diff --git a/tests/agents/test_host.py b/tests/agents/test_host.py new file mode 100644 index 00000000..e8b8736c --- /dev/null +++ b/tests/agents/test_host.py @@ -0,0 +1,595 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import datetime +import json +import os +import logging +from unittest.mock import Mock + +from mock import MagicMock, patch +import pytest +import requests + +from instana.agent.host import AnnounceData, HostAgent +from instana.agent.test import TestAgent +from instana.collector.host import HostCollector +from instana.fsm import Discovery, TheMachine +from instana.log import logger +from instana.options import StandardOptions +from instana.recorder import StanRecorder +from instana.sampling import InstanaSampler +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext +from instana.tracer import InstanaTracer + + +class TestHostAgent: + @pytest.fixture(autouse=True) + def _resource(self, caplog): + self.agent = None + self.span_recorder = None + self.tracer = None + self.original_agent = get_agent() + self.original_tracer = get_tracer() + yield + caplog.clear() + variable_names = ( + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_ENDPOINT_PROXY", + "INSTANA_AGENT_KEY", + "INSTANA_LOG_LEVEL", + "INSTANA_SERVICE_NAME", + "INSTANA_SECRETS", + "INSTANA_TAGS", + ) + + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = HostAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer( + sampler=InstanaSampler(), + span_processor=self.span_recorder, + exporter=TestAgent(), + propagators={}, + ) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_secrets(self): + self.create_agent_and_setup_tracer() + assert hasattr(self.agent.options, "secrets_matcher") + assert self.agent.options.secrets_matcher == "contains-ignore-case" + assert hasattr(self.agent.options, "secrets_list") + assert self.agent.options.secrets_list == ["key", "pass", "secret"] + + def test_options_have_extra_http_headers(self): + self.create_agent_and_setup_tracer() + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") + + def test_has_options(self): + self.create_agent_and_setup_tracer() + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, StandardOptions) + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.WARNING + + def test_agent_instana_debug(self): + os.environ["INSTANA_DEBUG"] = "asdf" + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.DEBUG + + def test_agent_instana_service_name(self): + os.environ["INSTANA_SERVICE_NAME"] = "greycake" + self.create_agent_and_setup_tracer() + assert self.agent.options.service_name == "greycake" + + @patch.object(requests.Session, "put") + def test_announce_is_successful(self, mock_requests_session_put): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = ( + "{" f' "pid": {test_pid}, ' f' "agentUuid": "{test_agent_uuid}"' "}" + ) + + # This mocks the call to self.agent.client.put + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) + + assert "pid" in payload + assert test_pid == payload["pid"] + + assert "agentUuid" in payload + assert test_agent_uuid == payload["agentUuid"] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_non_200(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 404 + mock_response.content = "" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response status code" in caplog.messages[0] + assert "is NOT 200" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_non_json(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response is not JSON" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_empty_list_json( + self, mock_requests_session_put, caplog + ): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "[]" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "payload has no fields" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_missing_pid(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "{" f' "agentUuid": "{test_agent_uuid}"' "}" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no pid" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_missing_uuid(self, mock_requests_session_put, caplog): + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "{" f' "pid": {test_pid} ' "}" + mock_requests_session_put.return_value = mock_response + + self.create_agent_and_setup_tracer() + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no agentUuid" in caplog.messages[0] + + @patch.object(requests.Session, "get") + def test_agent_connection_attempt(self, mock_requests_session_get, caplog): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_requests_session_get.return_value = mock_response + + self.create_agent_and_setup_tracer() + host = self.agent.options.agent_host + port = self.agent.options.agent_port + msg = f"Instana host agent found on {host}:{port}" + + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + result = self.agent.is_agent_listening(host, port) + + assert result + assert msg in caplog.messages[0] + + @patch.object(requests.Session, "get") + def test_agent_connection_attempt_fails_with_404( + self, mock_requests_session_get, caplog + ): + mock_response = MagicMock() + mock_response.status_code = 404 + mock_requests_session_get.return_value = mock_response + + self.create_agent_and_setup_tracer() + host = self.agent.options.agent_host + port = self.agent.options.agent_port + msg = ( + "The attempt to connect to the Instana host agent on " + f"{host}:{port} has failed with an unexpected status code. " + f"Expected HTTP 200 but received: {mock_response.status_code}" + ) + + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + result = self.agent.is_agent_listening(host, port) + + assert not result + assert msg in caplog.messages[0] + + def test_init(self): + with patch( + "instana.agent.base.BaseAgent.update_log_level" + ) as mock_update, patch.object(os, "getpid", return_value=12345): + agent = HostAgent() + assert not agent.announce_data + assert not agent.last_seen + assert not agent.last_fork_check + assert agent._boot_pid == 12345 + + mock_update.assert_called_once() + + assert isinstance(agent.options, StandardOptions) + assert isinstance(agent.collector, HostCollector) + assert isinstance(agent.machine, TheMachine) + + def test_start( + self, + ): + with patch("instana.collector.host.HostCollector.start") as mock_start: + agent = HostAgent() + agent.start() + mock_start.assert_called_once() + + def test_handle_fork( + self, + ): + with patch.object(HostAgent, "reset") as mock_reset: + agent = HostAgent() + agent.handle_fork() + mock_reset.assert_called_once() + + def test_reset( + self, + ): + with patch( + "instana.collector.host.HostCollector.shutdown" + ) as mock_shutdown, patch("instana.fsm.TheMachine.reset") as mock_reset: + agent = HostAgent() + agent.reset() + + assert not agent.last_seen + assert not agent.announce_data + + mock_shutdown.assert_called_once_with(report_final=False) + mock_reset.assert_called_once() + + def test_is_timed_out( + self, + ): + agent = HostAgent() + assert not agent.is_timed_out() + + agent.last_seen = datetime.datetime.now() - datetime.timedelta(minutes=5) + agent.can_send = True + assert agent.is_timed_out() + + def test_can_send_test_env( + self, + ): + agent = HostAgent() + with patch.dict("os.environ", {"INSTANA_TEST": "sample-data"}): + if "INSTANA_TEST" in os.environ: + assert agent.can_send() + + def test_can_send( + self, + ): + agent = HostAgent() + agent._boot_pid = 12345 + with patch.object(os, "getpid", return_value=12344), patch( + "instana.agent.host.HostAgent.handle_fork" + ) as mock_handle, patch.dict("os.environ", {}, clear=True): + agent.can_send() + assert agent._boot_pid == 12344 + mock_handle.assert_called_once() + + with patch.object(agent.machine.fsm, "current", "wait4init"): + assert agent.can_send() is True + + def test_can_send_default( + self, + ): + agent = HostAgent() + with patch.dict("os.environ", {}, clear=True): + assert not agent.can_send() + + def test_set_from( + self, + ): + agent = HostAgent() + sample_res_data = { + "secrets": {"matcher": "value-1", "list": ["value-2"]}, + "extraHeaders": ["value-3"], + "agentUuid": "value-4", + "pid": 1234, + } + agent.options.extra_http_headers = None + + agent.set_from(sample_res_data) + assert agent.options.secrets_matcher == "value-1" + assert agent.options.secrets_list == ["value-2"] + assert agent.options.extra_http_headers == ["value-3"] + + agent.options.extra_http_headers = ["value"] + agent.set_from(sample_res_data) + assert "value" in agent.options.extra_http_headers + + assert agent.announce_data.agentUuid == "value-4" + assert agent.announce_data.pid == 1234 + + def test_get_from_structure( + self, + ): + agent = HostAgent() + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + assert agent.get_from_structure() == {"e": 1234, "h": "value"} + + def test_is_agent_listening( + self, + caplog, + ): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + with patch.object(requests.Session, "get", return_value=mock_response): + assert agent.is_agent_listening("sample", 1234) + + mock_response.status_code = 404 + with patch.object( + requests.Session, "get", return_value=mock_response, clear=True + ): + assert not agent.is_agent_listening("sample", 1234) + + host = "localhost" + port = 123 + with patch.object(requests.Session, "get", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_listening(host, port) + assert f"Instana Host Agent not found on {host}:{port}" in caplog.messages + + def test_announce( + self, + caplog, + ): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + response = json.loads(mock_response.content) + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = mock_response.content.encode("UTF-8") + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + + with patch.object(requests.Session, "put", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + f"announce: connection error ({type(Exception())})" in caplog.messages + ) + + mock_response.content = json.dumps("key") + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert "announce: response payload has no fields: (key)" in caplog.messages + + mock_response.content = json.dumps({"key": "value"}) + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no pid: ({'key': 'value'})" + in caplog.messages + ) + + mock_response.content = json.dumps({"pid": "value"}) + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no agentUuid: ({'pid': 'value'})" + in caplog.messages + ) + + mock_response.status_code = 404 + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + assert not agent.announce("sample-data") + assert "announce: response status code (404) is NOT 200" in caplog.messages + + def test_log_message_to_host_agent( + self, + caplog, + ): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = "sample" + mock_datetime = datetime.datetime(2022, 1, 1, 12, 0, 0) + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.datetime" + ) as mock_date: + mock_date.now.return_value = mock_datetime + mock_date.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + agent.log_message_to_host_agent("sample") + assert agent.last_seen == mock_datetime + + with patch.object(requests.Session, "post", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.log_message_to_host_agent("sample") + assert ( + f"agent logging: connection error ({type(Exception())})" + in caplog.messages + ) + + def test_is_agent_ready(self, caplog): + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = {"key": "value"} + agent.AGENT_DATA_PATH = "sample_path" + agent.announce_data = AnnounceData(pid=1234, agentUuid="sample") + with patch.object(requests.Session, "head", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", + return_value="localhost", + ): + assert agent.is_agent_ready() + with patch.object(requests.Session, "head", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_ready() + assert ( + f"is_agent_ready: connection error ({type(Exception())})" + in caplog.messages + ) + + def test_report_data_payload( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ): + agent = HostAgent() + span_name = "test-span" + span_1 = InstanaSpan(span_name, span_context, span_processor) + span_2 = InstanaSpan(span_name, span_context, span_processor) + payload = { + "spans": [span_1, span_2], + "profiles": ["profile-1", "profile-2"], + "metrics": { + "plugins": [ + {"data": "sample data"}, + ] + }, + } + sample_response = {"key": "value"} + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = sample_response + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__traces_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__profiles_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", + return_value="localhost", + ): + test_response = agent.report_data_payload(payload) + assert isinstance(agent.last_seen, datetime.datetime) + assert test_response.content == sample_response + + def test_diagnostics(self, caplog): + caplog.set_level(logging.WARNING, logger="instana") + + agent = HostAgent() + agent.diagnostics() + assert ( + "====> Instana Python Language Agent Diagnostics <====" in caplog.messages + ) + assert "----> Agent <----" in caplog.messages + assert f"is_agent_ready: {agent.is_agent_ready()}" in caplog.messages + assert f"is_timed_out: {agent.is_timed_out()}" in caplog.messages + assert "last_seen: None" in caplog.messages + + sample_date = datetime.datetime(2022, 7, 25, 14, 30, 0) + agent.last_seen = sample_date + agent.diagnostics() + assert "last_seen: 2022-07-25 14:30:00" in caplog.messages + assert "announce_data: None" in caplog.messages + + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + agent.diagnostics() + assert f"announce_data: {agent.announce_data.__dict__}" in caplog.messages + assert f"Options: {agent.options.__dict__}" in caplog.messages + assert "----> StateMachine <----" in caplog.messages + assert f"State: {agent.machine.fsm.current}" in caplog.messages + assert "----> Collector <----" in caplog.messages + assert f"Collector: {agent.collector}" in caplog.messages + assert f"ready_to_start: {agent.collector.ready_to_start}" in caplog.messages + assert "reporting_thread: None" in caplog.messages + assert f"report_interval: {agent.collector.report_interval}" in caplog.messages + assert "should_send_snapshot_data: True" in caplog.messages diff --git a/tests/platforms/conftest.py b/tests/collector/conftest.py similarity index 100% rename from tests/platforms/conftest.py rename to tests/collector/conftest.py diff --git a/tests/collector/test_base_collector.py b/tests/collector/test_base_collector.py new file mode 100644 index 00000000..890fb4ca --- /dev/null +++ b/tests/collector/test_base_collector.py @@ -0,0 +1,200 @@ +import logging +import multiprocessing +import multiprocessing.queues +import queue +import threading +import time +from typing import Generator +from unittest.mock import patch + +import pytest +from instana.agent.test import TestAgent +from instana.collector.base import BaseCollector +from instana.recorder import StanRecorder +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan +from pytest import LogCaptureFixture + +from instana.span_context import SpanContext + + +class TestBaseCollector: + @pytest.fixture(autouse=True) + def _resource(self, caplog: LogCaptureFixture) -> Generator[None, None, None]: + self.collector = BaseCollector(TestAgent()) + yield + self.collector.shutdown(report_final=False) + self.collector = None + caplog.clear() + + def test_default(self) -> None: + assert isinstance(self.collector.agent, TestAgent) + assert self.collector.THREAD_NAME == "Instana Collector" + assert isinstance(self.collector.span_queue, multiprocessing.queues.Queue) + assert isinstance(self.collector.profile_queue, queue.Queue) + assert not self.collector.reporting_thread + assert isinstance(self.collector.thread_shutdown, threading.Event) + assert self.collector.snapshot_data_last_sent == 0 + assert self.collector.snapshot_data_interval == 300 + assert len(self.collector.helpers) == 0 + assert self.collector.report_interval == 1 + assert not self.collector.started + assert self.collector.fetching_start_time == 0 + + def test_default_env_is_prod(self): + with patch("instana.collector.base.env_is_test", False): + self.collector = BaseCollector(TestAgent()) + assert isinstance(self.collector.span_queue, queue.Queue) + + def test_is_reporting_thread_running(self) -> None: + stop_event = threading.Event() + + def reporting_function(): + stop_event.wait() + + sample_thread = threading.Thread( + name=self.collector.THREAD_NAME, target=reporting_function + ) + sample_thread.start() + try: + assert self.collector.is_reporting_thread_running() + finally: + stop_event.set() + sample_thread.join() + + def test_is_reporting_thread_running_with_different_name(self) -> None: + stop_event = threading.Event() + + def reporting_function(): + stop_event.wait() + + sample_thread = threading.Thread(name="test-thread", target=reporting_function) + sample_thread.start() + try: + assert not self.collector.is_reporting_thread_running() + finally: + stop_event.set() + sample_thread.join() + + def test_start_collector_while_running_thread( + self, caplog: LogCaptureFixture + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=True, + ): + self.collector.start() + assert ( + "BaseCollector.start non-fatal: call but thread already running (started: False)" + in caplog.messages + ) + + def test_start_agent_shutdown_is_set(self) -> None: + self.collector.thread_shutdown.set() + isThreadFound = False + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=True, + ): + response = self.collector.start() + assert not response + for thread in threading.enumerate(): + if thread.name == "Collector Timed Start": + isThreadFound = True + assert isThreadFound + + def test_start_collector_when_agent_is_ready( + self, caplog: LogCaptureFixture + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + if not self.collector.started: + self.collector.start() + assert "BaseCollector.start: launching collection thread" in caplog.messages + assert self.collector.started + assert self.collector.reporting_thread.daemon + assert self.collector.reporting_thread.name == self.collector.THREAD_NAME + + def test_start_agent_can_not_send(self, caplog: LogCaptureFixture) -> None: + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=False, + ), patch("instana.agent.host.HostAgent.can_send", return_value=False): + caplog.set_level(logging.WARNING, logger="instana") + self.collector.agent.machine.fsm.current = "test" + self.collector.start() + assert ( + "BaseCollector.start: the agent tells us we can't send anything out" + in caplog.messages + ) + + def test_shutdown(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.shutdown() + assert "Collector.shutdown: Reporting final data." in caplog.messages + assert not self.collector.started + + def test_background_report(self) -> None: + assert self.collector.background_report() + self.collector.thread_shutdown.set() + assert not self.collector.background_report() + + def test_prepare_and_report_data(self) -> None: + assert self.collector.prepare_and_report_data() + with patch("instana.collector.base.env_is_test", False): + assert self.collector.prepare_and_report_data() + + def test_should_send_snapshot_data(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.should_send_snapshot_data() + assert ( + "BaseCollector: should_send_snapshot_data needs to be overridden" + in caplog.messages + ) + + def test_collect_snapshot(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.collect_snapshot() + assert ( + "BaseCollector: collect_snapshot needs to be overridden" in caplog.messages + ) + + def test_queued_spans( + self, span_context: SpanContext, span_processor: StanRecorder + ) -> None: + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + for span in span_list: + self.collector.span_queue.put(span) + time.sleep(0.1) + spans = self.collector.queued_spans() + assert len(spans) == 3 + + def test_queued_profiles( + self, span_context: SpanContext, span_processor: StanRecorder + ) -> None: + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + for span in span_list: + self.collector.profile_queue.put(span) + time.sleep(0.1) + profiles = self.collector.queued_profiles() + assert len(profiles) == 3 diff --git a/tests/platforms/test_eksfargate_collector.py b/tests/collector/test_eksfargate_collector.py similarity index 100% rename from tests/platforms/test_eksfargate_collector.py rename to tests/collector/test_eksfargate_collector.py diff --git a/tests/collector/test_host_collector.py b/tests/collector/test_host_collector.py new file mode 100644 index 00000000..0802f607 --- /dev/null +++ b/tests/collector/test_host_collector.py @@ -0,0 +1,303 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import logging +import os +import sys +import threading +from typing import Generator + +import pytest +from instana.agent.host import HostAgent +from instana.collector.helpers.runtime import ( + PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR, +) +from instana.collector.host import HostCollector +from instana.recorder import StanRecorder +from instana.singletons import get_agent, get_tracer, set_agent, set_tracer +from instana.tracer import InstanaTracer, InstanaTracerProvider +from instana.version import VERSION +from mock import patch +from pytest import LogCaptureFixture + + +class TestHostCollector: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.collector = HostCollector(HostAgent()) + self.original_agent = get_agent() + self.original_tracer = get_tracer() + self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + "3.8.0" + yield + self.collector.shutdown(report_final=False) + variable_names = ( + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_AGENT_KEY", + "INSTANA_ZONE", + "INSTANA_TAGS", + "INSTANA_DISABLE_METRICS_COLLECTION", + "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", + "AUTOWRAPT_BOOTSTRAP", + ) + + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + if self.webhook_sitedir_path in sys.path: + sys.path.remove(self.webhook_sitedir_path) + + def create_agent_and_setup_tracer( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.agent = HostAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer( + tracer_provider.sampler, + tracer_provider._span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_start(self) -> None: + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=False, + ): + self.collector.start() + assert self.collector.started + assert self.collector.THREAD_NAME == "Instana Collector" + assert self.collector.snapshot_data_interval == 300 + assert self.collector.snapshot_data_last_sent == 0 + assert isinstance(self.collector.helpers[0].collector, HostCollector) + assert len(self.collector.helpers) == 1 + assert isinstance(self.collector.reporting_thread, threading.Thread) + self.collector.ready_to_start = False + assert not self.collector.start() + + def test_prepare_and_report_data(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.agent.machine.fsm.current = "wait4init" + with patch("instana.agent.host.HostAgent.is_agent_ready", return_value=True): + self.collector.prepare_and_report_data() + assert "Agent is ready. Getting to work." in caplog.messages + assert "Harmless state machine thread disagreement. Will self-correct on next timer cycle." + self.collector.agent.machine.fsm.current = "wait4init" + with patch("instana.agent.host.HostAgent.is_agent_ready", return_value=False): + assert not self.collector.prepare_and_report_data() + self.collector.agent.machine.fsm.current = "good2go" + caplog.clear() + with patch("instana.agent.host.HostAgent.is_timed_out", return_value=True): + self.collector.prepare_and_report_data() + assert ( + "The Instana host agent has gone offline or is no longer reachable for > 1 min. Will retry periodically." + in caplog.messages + ) + + def test_should_send_snapshot_data(self) -> None: + self.collector.snapshot_data_interval = 999999999999 + assert not self.collector.should_send_snapshot_data() + + def test_prepare_payload_basics( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + + assert len(payload.keys()) == 3 + assert "spans" in payload + assert isinstance(payload["spans"], list) + assert len(payload["spans"]) == 0 + assert "metrics", payload + assert len(payload["metrics"].keys()) == 1 + assert "plugins", payload["metrics"] + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + + python_plugin = payload["metrics"]["plugins"][0] + assert python_plugin["name"] == "com.instana.plugin.python" + assert python_plugin["entityId"] == str(os.getpid()) + assert "data" in python_plugin + assert "snapshot" in python_plugin["data"] + assert "m" in python_plugin["data"]["snapshot"] + assert "Manual" == python_plugin["data"]["snapshot"]["m"] + assert "metrics" in python_plugin["data"] + + assert "ru_utime" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_utime"]) in [float, int] + assert "ru_stime" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_stime"]) in [float, int] + assert "ru_maxrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_maxrss"]) in [float, int] + assert "ru_ixrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_ixrss"]) in [float, int] + assert "ru_idrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_idrss"]) in [float, int] + assert "ru_isrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_isrss"]) in [float, int] + assert "ru_minflt" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_minflt"]) in [float, int] + assert "ru_majflt" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_majflt"]) in [float, int] + assert "ru_nswap" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nswap"]) in [float, int] + assert "ru_inblock" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_inblock"]) in [float, int] + assert "ru_oublock" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_oublock"]) in [float, int] + assert "ru_msgsnd" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_msgsnd"]) in [float, int] + assert "ru_msgrcv" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_msgrcv"]) in [float, int] + assert "ru_nsignals" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nsignals"]) in [float, int] + assert "ru_nvcsw" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nvcsw"]) in [float, int] + assert "ru_nivcsw" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nivcsw"]) in [float, int] + assert "alive_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["alive_threads"]) in [float, int] + assert "dummy_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["dummy_threads"]) in [float, int] + assert "daemon_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["daemon_threads"]) in [float, int] + + assert "gc" in python_plugin["data"]["metrics"] + assert isinstance(python_plugin["data"]["metrics"]["gc"], dict) + assert "collect0" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect0"]) in [float, int] + assert "collect1" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect1"]) in [float, int] + assert "collect2" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect2"]) in [float, int] + assert "threshold0" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold0"]) in [ + float, + int, + ] + assert "threshold1" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold1"]) in [ + float, + int, + ] + assert "threshold2" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold2"]) in [ + float, + int, + ] + + def test_prepare_payload_basics_disable_runtime_metrics( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["INSTANA_DISABLE_METRICS_COLLECTION"] = "TRUE" + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + + assert len(payload.keys()) == 3 + assert "spans" in payload + assert isinstance(payload["spans"], list) + assert len(payload["spans"]) == 0 + assert "metrics" in payload + assert len(payload["metrics"].keys()) == 1 + assert "plugins" in payload["metrics"] + assert isinstance(payload["metrics"]["plugins"], list) + assert len(payload["metrics"]["plugins"]) == 1 + + python_plugin = payload["metrics"]["plugins"][0] + assert python_plugin["name"] == "com.instana.plugin.python" + assert python_plugin["entityId"] == str(os.getpid()) + assert "data" in python_plugin + assert "snapshot" in python_plugin["data"] + assert "m" in python_plugin["data"]["snapshot"] + assert "Manual" == python_plugin["data"]["snapshot"]["m"] + assert "metrics" not in python_plugin["data"] + + def test_prepare_payload_with_snapshot_with_python_packages( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Manual" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + assert snapshot["versions"]["instana"] == VERSION + assert "wrapt" in snapshot["versions"] + assert "fysom" in snapshot["versions"] + + def test_prepare_payload_with_snapshot_disabled_python_packages( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"] = "TRUE" + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Manual" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) == 1 + assert snapshot["versions"]["instana"] == VERSION + + def test_prepare_payload_with_autowrapt( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["AUTOWRAPT_BOOTSTRAP"] = "instana" + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Autowrapt" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + expected_packages = ("instana", "wrapt", "fysom") + for package in expected_packages: + assert ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" + assert snapshot["versions"]["instana"] == VERSION + + def test_prepare_payload_with_autotrace( + self, tracer_provider: InstanaTracerProvider + ) -> None: + sys.path.append(self.webhook_sitedir_path) + + self.create_agent_and_setup_tracer(tracer_provider) + + payload = self.agent.collector.prepare_payload() + assert payload + assert "snapshot" in payload["metrics"]["plugins"][0]["data"] + snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "AutoTrace" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + expected_packages = ("instana", "wrapt", "fysom") + for package in expected_packages: + assert ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" + assert snapshot["versions"]["instana"] == VERSION diff --git a/tests/collector/test_runtime.py b/tests/collector/test_runtime.py new file mode 100644 index 00000000..b42ad1b4 --- /dev/null +++ b/tests/collector/test_runtime.py @@ -0,0 +1,65 @@ +from typing import Generator +from unittest.mock import patch +import pytest + +from instana.agent.host import HostAgent +from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.host import HostCollector + + +class TestRuntimeHelper: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.helper = RuntimeHelper( + collector=HostCollector( + HostAgent(), + ), + ) + yield + self.helper = None + + def test_default_while_gc_disabled(self) -> None: + import gc + + gc.disable() + helper = RuntimeHelper(collector=HostCollector(HostAgent())) + assert helper.previous_gc_count is None + + def test_collect_metrics(self) -> None: + response = self.helper.collect_metrics() + assert response[0]["name"] == "com.instana.plugin.python" + + def test_collect_runtime_snapshot_default(self) -> None: + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "Manual" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_runtime_snapshot_autowrapt(self) -> None: + with patch( + "instana.collector.helpers.runtime.is_autowrapt_instrumented", + return_value=True, + ): + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "Autowrapt" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_runtime_snapshot_webhook(self) -> None: + with patch( + "instana.collector.helpers.runtime.is_webhook_instrumented", + return_value=True, + ): + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "AutoTrace" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_gc_metrics(self) -> None: + plugin_data = self.helper.collect_metrics() + + self.helper._collect_gc_metrics(plugin_data[0], True) + assert len(self.helper.previous["data"]["metrics"]["gc"]) == 6 diff --git a/tests/collector/test_utils.py b/tests/collector/test_utils.py new file mode 100644 index 00000000..d8b4ad40 --- /dev/null +++ b/tests/collector/test_utils.py @@ -0,0 +1,34 @@ +import pytest +from typing import Generator +from instana.collector.utils import format_span +from instana.singletons import tracer +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan +from opentelemetry.trace.span import format_span_id + + +class TestUtils: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.recorder = tracer.span_processor + yield + + def test_format_span(self, trace_id, span_id, span_context, span_processor) -> None: + expected_trace_id = format_span_id(trace_id) + expected_span_id = format_span_id(span_id) + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + formatted_spans = format_span(span_list) + assert len(formatted_spans) == 3 + assert formatted_spans[0].t == expected_trace_id + assert formatted_spans[0].s == expected_span_id + assert formatted_spans[0].n == "span1" diff --git a/tests/platforms/__init__.py b/tests/platforms/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/platforms/test_eksfargate.py b/tests/platforms/test_eksfargate.py deleted file mode 100644 index 9d6e2437..00000000 --- a/tests/platforms/test_eksfargate.py +++ /dev/null @@ -1,120 +0,0 @@ -# (c) Copyright IBM Corp. 2024 - -import os -import logging -import unittest - -from instana.tracer import InstanaTracer -from instana.options import EKSFargateOptions -from instana.recorder import StanRecorder -from instana.agent.aws_eks_fargate import EKSFargateAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestFargate(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestFargate, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["INSTANA_TRACER_ENVIRONMENT"] = "AWS_EKS_FARGATE" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - def tearDown(self): - """ Reset all environment variables of consequence """ - variable_names = ( - "INSTANA_TRACER_ENVIRONMENT", - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_ENDPOINT_PROXY", - "INSTANA_AGENT_KEY", "INSTANA_LOG_LEVEL", - "INSTANA_SECRETS", "INSTANA_DEBUG", "INSTANA_TAGS" - ) - - for variable_name in variable_names: - if variable_name in os.environ: - os.environ.pop(variable_name) - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = EKSFargateAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, EKSFargateOptions)) - - def test_missing_variables(self): - with self.assertLogs("instana", level=logging.WARN) as context: - os.environ.pop("INSTANA_ENDPOINT_URL") - agent = EKSFargateAgent() - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - self.assertIn('environment variables not set', context.output[0]) - - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - with self.assertLogs("instana", level=logging.WARN) as context: - os.environ.pop("INSTANA_AGENT_KEY") - agent = EKSFargateAgent() - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - self.assertIn('environment variables not set', context.output[0]) - - def test_default_secrets(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.secrets) - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertListEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_custom_secrets(self): - os.environ["INSTANA_SECRETS"] = "equals:love,war,games" - self.create_agent_and_setup_tracer() - - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'equals') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertListEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) - - def test_default_tags(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'tags')) - self.assertIsNone(self.agent.options.tags) - - def test_has_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) - - def test_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertListEqual(should_headers, self.agent.options.extra_http_headers) - - def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.WARNING) - - def test_agent_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.ERROR) - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - self.assertDictEqual(self.agent.options.endpoint_proxy, {'https': "http://myproxy.123"}) diff --git a/tests/platforms/test_fargate.py b/tests/platforms/test_fargate.py deleted file mode 100644 index 7a50353a..00000000 --- a/tests/platforms/test_fargate.py +++ /dev/null @@ -1,125 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import logging -import unittest - -from instana.tracer import InstanaTracer -from instana.options import AWSFargateOptions -from instana.recorder import StanRecorder -from instana.agent.aws_fargate import AWSFargateAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestFargate(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestFargate, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "AWS_EXECUTION_ENV" in os.environ: - os.environ.pop("AWS_EXECUTION_ENV") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_ENDPOINT_PROXY" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_PROXY") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_LOG_LEVEL" in os.environ: - os.environ.pop("INSTANA_LOG_LEVEL") - if "INSTANA_SECRETS" in os.environ: - os.environ.pop("INSTANA_SECRETS") - if "INSTANA_DEBUG" in os.environ: - os.environ.pop("INSTANA_DEBUG") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = AWSFargateAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, AWSFargateOptions)) - - def test_invalid_options(self): - # None of the required env vars are available... - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - - agent = AWSFargateAgent() - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - - def test_default_secrets(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.secrets) - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertListEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_custom_secrets(self): - os.environ["INSTANA_SECRETS"] = "equals:love,war,games" - self.create_agent_and_setup_tracer() - - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'equals') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertListEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) - - def test_default_tags(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'tags')) - self.assertIsNone(self.agent.options.tags) - - def test_has_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) - - def test_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertListEqual(should_headers, self.agent.options.extra_http_headers) - - def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.WARNING) - - def test_agent_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.ERROR) - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - self.assertDictEqual(self.agent.options.endpoint_proxy, {'https': "http://myproxy.123"}) diff --git a/tests/platforms/test_fargate_collector.py b/tests/platforms/test_fargate_collector.py deleted file mode 100644 index 361d14e8..00000000 --- a/tests/platforms/test_fargate_collector.py +++ /dev/null @@ -1,242 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import json -import unittest - -from instana.tracer import InstanaTracer -from instana.recorder import StanRecorder -from instana.agent.aws_fargate import AWSFargateAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -def get_docker_plugin(plugins): - """ - Given a list of plugins, find and return the docker plugin that we're interested in from the mock data - """ - docker_plugin = None - for plugin in plugins: - if plugin["name"] == "com.instana.plugin.docker" and plugin["entityId"] == "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82::docker-ssh-aws-fargate": - docker_plugin = plugin - return docker_plugin - - -class TestFargateCollector(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestFargateCollector, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - self.pwd = os.path.dirname(os.path.realpath(__file__)) - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - if "INSTANA_ZONE" in os.environ: - os.environ.pop("INSTANA_ZONE") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "AWS_EXECUTION_ENV" in os.environ: - os.environ.pop("AWS_EXECUTION_ENV") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_ZONE" in os.environ: - os.environ.pop("INSTANA_ZONE") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = AWSFargateAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - # Manually set the ECS Metadata API results on the collector - with open(self.pwd + '/../data/fargate/1.3.0/root_metadata.json', 'r') as json_file: - self.agent.collector.root_metadata = json.load(json_file) - with open(self.pwd + '/../data/fargate/1.3.0/task_metadata.json', 'r') as json_file: - self.agent.collector.task_metadata = json.load(json_file) - with open(self.pwd + '/../data/fargate/1.3.0/stats_metadata.json', 'r') as json_file: - self.agent.collector.stats_metadata = json.load(json_file) - with open(self.pwd + '/../data/fargate/1.3.0/task_stats_metadata.json', 'r') as json_file: - self.agent.collector.task_stats_metadata = json.load(json_file) - - def test_prepare_payload_basics(self): - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(2, len(payload.keys())) - self.assertIn('spans',payload) - self.assertIsInstance(payload['spans'], list) - self.assertEqual(0, len(payload['spans'])) - self.assertIn('metrics', payload) - self.assertEqual(1, len(payload['metrics'].keys())) - self.assertIn('plugins', payload['metrics']) - self.assertIsInstance(payload['metrics']['plugins'], list) - self.assertEqual(7, len(payload['metrics']['plugins'])) - - plugins = payload['metrics']['plugins'] - for plugin in plugins: - # print("%s - %s" % (plugin["name"], plugin["entityId"])) - self.assertIn('name', plugin) - self.assertIn('entityId', plugin) - self.assertIn('data', plugin) - - def test_docker_plugin_snapshot_data(self): - self.create_agent_and_setup_tracer() - - first_payload = self.agent.collector.prepare_payload() - second_payload = self.agent.collector.prepare_payload() - - self.assertTrue(first_payload) - self.assertTrue(second_payload) - - plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) - plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) - - self.assertTrue(plugin_first_report) - self.assertIn("data", plugin_first_report) - - # First report should have snapshot data - data = plugin_first_report["data"] - self.assertEqual(data["Id"], "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45") - self.assertEqual(data["Created"], "2020-07-27T12:14:12.583114444Z") - self.assertEqual(data["Started"], "2020-07-27T12:14:13.545410186Z") - self.assertEqual(data["Image"], "410797082306.dkr.ecr.us-east-2.amazonaws.com/fargate-docker-ssh:latest") - self.assertEqual(data["Labels"], {'com.amazonaws.ecs.cluster': 'arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster', 'com.amazonaws.ecs.container-name': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-arn': 'arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82', 'com.amazonaws.ecs.task-definition-family': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-definition-version': '1'}) - self.assertIsNone(data["Ports"]) - - # Second report should have no snapshot data - self.assertTrue(plugin_second_report) - self.assertIn("data", plugin_second_report) - data = plugin_second_report["data"] - self.assertIn("Id", data) - self.assertNotIn("Created", data) - self.assertNotIn("Started", data) - self.assertNotIn("Image", data) - self.assertNotIn("Labels", data) - self.assertNotIn("Ports", data) - - def test_docker_plugin_metrics(self): - self.create_agent_and_setup_tracer() - - first_payload = self.agent.collector.prepare_payload() - second_payload = self.agent.collector.prepare_payload() - - self.assertTrue(first_payload) - self.assertTrue(second_payload) - - plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) - self.assertTrue(plugin_first_report) - self.assertIn("data", plugin_first_report) - - plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) - self.assertTrue(plugin_second_report) - self.assertIn("data", plugin_second_report) - - # First report should report all metrics - data = plugin_first_report.get("data", None) - self.assertTrue(data) - self.assertNotIn("network", data) - - cpu = data.get("cpu", None) - self.assertTrue(cpu) - self.assertEqual(cpu["total_usage"], 0.011033) - self.assertEqual(cpu["user_usage"], 0.009918) - self.assertEqual(cpu["system_usage"], 0.00089) - self.assertEqual(cpu["throttling_count"], 0) - self.assertEqual(cpu["throttling_time"], 0) - - memory = data.get("memory", None) - self.assertTrue(memory) - self.assertEqual(memory["active_anon"], 78721024) - self.assertEqual(memory["active_file"], 18501632) - self.assertEqual(memory["inactive_anon"], 0) - self.assertEqual(memory["inactive_file"], 71684096) - self.assertEqual(memory["total_cache"], 90185728) - self.assertEqual(memory["total_rss"], 78721024) - self.assertEqual(memory["usage"], 193769472) - self.assertEqual(memory["max_usage"], 195305472) - self.assertEqual(memory["limit"], 536870912) - - blkio = data.get("blkio", None) - self.assertTrue(blkio) - self.assertEqual(blkio["blk_read"], 0) - self.assertEqual(blkio["blk_write"], 128352256) - - # Second report should report the delta (in the test case, nothing) - data = plugin_second_report["data"] - self.assertIn("cpu", data) - self.assertEqual(len(data["cpu"]), 0) - self.assertIn("memory", data) - self.assertEqual(len(data["memory"]), 0) - self.assertIn("blkio", data) - self.assertEqual(len(data["blkio"]), 1) - self.assertEqual(data["blkio"]['blk_write'], 0) - self.assertNotIn('blk_read', data["blkio"]) - - def test_no_instana_zone(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.zone) - - def test_instana_zone(self): - os.environ["INSTANA_ZONE"] = "YellowDog" - self.create_agent_and_setup_tracer() - - self.assertEqual(self.agent.options.zone, "YellowDog") - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - plugins = payload['metrics']['plugins'] - self.assertIsInstance(plugins, list) - - task_plugin = None - for plugin in plugins: - if plugin["name"] == "com.instana.plugin.aws.ecs.task": - task_plugin = plugin - - self.assertTrue(task_plugin) - self.assertIn("data", task_plugin) - self.assertIn("instanaZone", task_plugin["data"]) - self.assertEqual(task_plugin["data"]["instanaZone"], "YellowDog") - - def test_custom_tags(self): - os.environ["INSTANA_TAGS"] = "love,war=1,games" - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'tags')) - self.assertDictEqual(self.agent.options.tags, {"love": None, "war": "1", "games": None}) - - payload = self.agent.collector.prepare_payload() - - self.assertTrue(payload) - task_plugin = None - plugins = payload['metrics']['plugins'] - for plugin in plugins: - if plugin["name"] == "com.instana.plugin.aws.ecs.task": - task_plugin = plugin - self.assertTrue(task_plugin) - self.assertIn("tags", task_plugin["data"]) - tags = task_plugin["data"]["tags"] - self.assertEqual(tags["war"], "1") - self.assertIsNone(tags["love"]) - self.assertIsNone(tags["games"]) diff --git a/tests/platforms/test_gcr_collector.py b/tests/platforms/test_gcr_collector.py deleted file mode 100644 index 39c7e886..00000000 --- a/tests/platforms/test_gcr_collector.py +++ /dev/null @@ -1,95 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2021 - -import os -import json -import unittest - -import requests_mock - -from instana.tracer import InstanaTracer -from instana.recorder import StanRecorder -from instana.agent.google_cloud_run import GCRAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestGCRCollector(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestGCRCollector, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - self.pwd = os.path.dirname(os.path.realpath(__file__)) - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["PORT"] = "port" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - if "INSTANA_ZONE" in os.environ: - os.environ.pop("INSTANA_ZONE") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "PORT" in os.environ: - os.environ.pop("PORT") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_ZONE" in os.environ: - os.environ.pop("INSTANA_ZONE") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = GCRAgent(service="service", configuration="configuration", revision="revision") - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - # Manually set the Instance and Project Metadata API results on the collector - with open(self.pwd + '/../data/gcr/instance_metadata.json', 'r') as json_file: - self.agent.collector.instance_metadata = json.load(json_file) - with open(self.pwd + '/../data/gcr/project_metadata.json', 'r') as json_file: - self.agent.collector.project_metadata = json.load(json_file) - - @requests_mock.Mocker() - def test_prepare_payload_basics(self, m): - self.create_agent_and_setup_tracer() - m.get("http://metadata.google.internal/computeMetadata/v1/project/?recursive=true", - headers={"Metadata-Flavor": "Google"}, json=self.agent.collector.project_metadata) - - m.get("http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true", - headers={"Metadata-Flavor": "Google"}, json=self.agent.collector.instance_metadata) - - payload = self.agent.collector.prepare_payload() - assert (payload) - - assert (len(payload.keys()) == 2) - assert ('spans' in payload) - assert (isinstance(payload['spans'], list)) - assert (len(payload['spans']) == 0) - assert ('metrics' in payload) - assert (len(payload['metrics'].keys()) == 1) - assert ('plugins' in payload['metrics']) - assert (isinstance(payload['metrics']['plugins'], list)) - assert (len(payload['metrics']['plugins']) == 2) - - plugins = payload['metrics']['plugins'] - for plugin in plugins: - # print("%s - %s" % (plugin["name"], plugin["entityId"])) - assert ('name' in plugin) - assert ('entityId' in plugin) - assert ('data' in plugin) diff --git a/tests/platforms/test_google_cloud_run.py b/tests/platforms/test_google_cloud_run.py deleted file mode 100644 index 8b086a70..00000000 --- a/tests/platforms/test_google_cloud_run.py +++ /dev/null @@ -1,129 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2021 - -import os -import logging -import unittest - -from instana.tracer import InstanaTracer -from instana.options import GCROptions -from instana.recorder import StanRecorder -from instana.agent.google_cloud_run import GCRAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestGCR(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestGCR, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["K_SERVICE"] = "service" - os.environ["K_CONFIGURATION"] = "configuration" - os.environ["K_REVISION"] = "revision" - os.environ["PORT"] = "port" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "K_SERVICE" in os.environ: - os.environ.pop("K_SERVICE") - if "K_CONFIGURATION" in os.environ: - os.environ.pop("K_CONFIGURATION") - if "K_REVISION" in os.environ: - os.environ.pop("K_REVISION") - if "PORT" in os.environ: - os.environ.pop("PORT") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_ENDPOINT_PROXY" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_PROXY") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_LOG_LEVEL" in os.environ: - os.environ.pop("INSTANA_LOG_LEVEL") - if "INSTANA_SECRETS" in os.environ: - os.environ.pop("INSTANA_SECRETS") - if "INSTANA_DEBUG" in os.environ: - os.environ.pop("INSTANA_DEBUG") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = GCRAgent(service="service", configuration="configuration", revision="revision") - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, GCROptions)) - - def test_invalid_options(self): - # None of the required env vars are available... - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - - agent = GCRAgent(service="service", configuration="configuration", revision="revision") - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - - def test_default_secrets(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.secrets) - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_custom_secrets(self): - os.environ["INSTANA_SECRETS"] = "equals:love,war,games" - self.create_agent_and_setup_tracer() - - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'equals') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) - - def test_has_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) - - def test_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertEqual(should_headers, self.agent.options.extra_http_headers) - - def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - assert self.agent.options.log_level == logging.WARNING - - def test_agent_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - assert self.agent.options.log_level == logging.ERROR - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - assert self.agent.options.endpoint_proxy == {'https': "http://myproxy.123"} diff --git a/tests/platforms/test_host.py b/tests/platforms/test_host.py deleted file mode 100644 index 2ca09804..00000000 --- a/tests/platforms/test_host.py +++ /dev/null @@ -1,273 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import logging -import unittest - -from mock import MagicMock, patch -import requests - -from instana.agent.host import HostAgent -from instana.fsm import Discovery -from instana.log import logger -from instana.options import StandardOptions -from instana.recorder import StanRecorder -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer -from instana.tracer import InstanaTracer - - -class TestHost(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestHost, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - pass - - def tearDown(self): - """ Reset all environment variables of consequence """ - variable_names = ( - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_ENDPOINT_PROXY", - "INSTANA_AGENT_KEY", "INSTANA_LOG_LEVEL", - "INSTANA_SERVICE_NAME", "INSTANA_SECRETS", "INSTANA_TAGS", - ) - - for variable_name in variable_names: - if variable_name in os.environ: - os.environ.pop(variable_name) - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = HostAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_secrets(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_options_have_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, StandardOptions)) - - def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.WARNING) - - def test_agent_instana_debug(self): - os.environ['INSTANA_DEBUG'] = "asdf" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.DEBUG) - - def test_agent_instana_service_name(self): - os.environ['INSTANA_SERVICE_NAME'] = "greycake" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.service_name, "greycake") - - @patch.object(requests.Session, "put") - def test_announce_is_successful(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "pid": {test_pid}, ' - f' "agentUuid": "{test_agent_uuid}"' - '}') - - # This mocks the call to self.agent.client.put - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - payload = self.agent.announce(d) - - self.assertIn('pid', payload) - self.assertEqual(test_pid, payload['pid']) - - self.assertIn('agentUuid', payload) - self.assertEqual(test_agent_uuid, payload['agentUuid']) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_non_200(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 404 - mock_response.content = '' - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response status code', log.output[0]) - self.assertIn('is NOT 200', log.output[0]) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_non_json(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = '' - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response is not JSON', log.output[0]) - - @patch.object(requests.Session, "put") - def test_announce_fails_with_empty_list_json(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = '[]' - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('payload has no fields', log.output[0]) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_missing_pid(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "agentUuid": "{test_agent_uuid}"' - '}') - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response payload has no pid', log.output[0]) - - - @patch.object(requests.Session, "put") - def test_announce_fails_with_missing_uuid(self, mock_requests_session_put): - test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' - - mock_response = MagicMock() - mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "pid": {test_pid} ' - '}') - mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response payload has no agentUuid', log.output[0]) - - - @patch.object(requests.Session, "get") - def test_agent_connection_attempt(self, mock_requests_session_get): - mock_response = MagicMock() - mock_response.status_code = 200 - mock_requests_session_get.return_value = mock_response - - self.create_agent_and_setup_tracer() - host = self.agent.options.agent_host - port = self.agent.options.agent_port - msg = f"Instana host agent found on {host}:{port}" - - with self.assertLogs(logger, level='DEBUG') as log: - result = self.agent.is_agent_listening(host, port) - - self.assertTrue(result) - self.assertIn(msg, log.output[0]) - - - @patch.object(requests.Session, "get") - def test_agent_connection_attempt_fails_with_404(self, mock_requests_session_get): - mock_response = MagicMock() - mock_response.status_code = 404 - mock_requests_session_get.return_value = mock_response - - self.create_agent_and_setup_tracer() - host = self.agent.options.agent_host - port = self.agent.options.agent_port - msg = "The attempt to connect to the Instana host agent on " \ - f"{host}:{port} has failed with an unexpected status code. " \ - f"Expected HTTP 200 but received: {mock_response.status_code}" - - with self.assertLogs(logger, level='DEBUG') as log: - result = self.agent.is_agent_listening(host, port) - - self.assertFalse(result) - self.assertIn(msg, log.output[0]) diff --git a/tests/platforms/test_host_collector.py b/tests/platforms/test_host_collector.py deleted file mode 100644 index 667e7afd..00000000 --- a/tests/platforms/test_host_collector.py +++ /dev/null @@ -1,281 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import unittest -import sys - -from mock import patch - -from instana.tracer import InstanaTracer -from instana.recorder import StanRecorder -from instana.agent.host import HostAgent -from instana.collector.helpers.runtime import ( - PATH_OF_DEPRECATED_INSTALLATION_VIA_HOST_AGENT, -) -from instana.collector.host import HostCollector -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer -from instana.version import VERSION - - -class TestHostCollector(unittest.TestCase): - def __init__(self, methodName="runTest"): - super(TestHostCollector, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + '3.8.0' - - def tearDown(self): - """Reset all environment variables of consequence""" - variable_names = ( - "AWS_EXECUTION_ENV", - "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", - "INSTANA_AGENT_KEY", - "INSTANA_ZONE", - "INSTANA_TAGS", - "INSTANA_DISABLE_METRICS_COLLECTION", - "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", - "AUTOWRAPT_BOOTSTRAP", - ) - - for variable_name in variable_names: - if variable_name in os.environ: - os.environ.pop(variable_name) - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - if self.webhook_sitedir_path in sys.path: - sys.path.remove(self.webhook_sitedir_path) - - def create_agent_and_setup_tracer(self): - self.agent = HostAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_prepare_payload_basics(self): - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(len(payload.keys()), 3) - self.assertIn("spans", payload) - self.assertIsInstance(payload["spans"], list) - self.assertEqual(len(payload["spans"]), 0) - self.assertIn("metrics", payload) - self.assertEqual(len(payload["metrics"].keys()), 1) - self.assertIn("plugins", payload["metrics"]) - self.assertIsInstance(payload["metrics"]["plugins"], list) - self.assertEqual(len(payload["metrics"]["plugins"]), 1) - - python_plugin = payload["metrics"]["plugins"][0] - self.assertEqual(python_plugin["name"], "com.instana.plugin.python") - self.assertEqual(python_plugin["entityId"], str(os.getpid())) - self.assertIn("data", python_plugin) - self.assertIn("snapshot", python_plugin["data"]) - self.assertIn("m", python_plugin["data"]["snapshot"]) - self.assertEqual("Manual", python_plugin["data"]["snapshot"]["m"]) - self.assertIn("metrics", python_plugin["data"]) - - # Validate that all metrics are reported on the first run - self.assertIn("ru_utime", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_utime"]), [float, int]) - self.assertIn("ru_stime", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_stime"]), [float, int]) - self.assertIn("ru_maxrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_maxrss"]), [float, int]) - self.assertIn("ru_ixrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_ixrss"]), [float, int]) - self.assertIn("ru_idrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_idrss"]), [float, int]) - self.assertIn("ru_isrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_isrss"]), [float, int]) - self.assertIn("ru_minflt", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_minflt"]), [float, int]) - self.assertIn("ru_majflt", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_majflt"]), [float, int]) - self.assertIn("ru_nswap", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_nswap"]), [float, int]) - self.assertIn("ru_inblock", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["ru_inblock"]), [float, int] - ) - self.assertIn("ru_oublock", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["ru_oublock"]), [float, int] - ) - self.assertIn("ru_msgsnd", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_msgsnd"]), [float, int]) - self.assertIn("ru_msgrcv", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_msgrcv"]), [float, int]) - self.assertIn("ru_nsignals", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["ru_nsignals"]), [float, int] - ) - self.assertIn("ru_nvcsw", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_nvcsw"]), [float, int]) - self.assertIn("ru_nivcsw", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_nivcsw"]), [float, int]) - self.assertIn("alive_threads", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["alive_threads"]), [float, int] - ) - self.assertIn("dummy_threads", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["dummy_threads"]), [float, int] - ) - self.assertIn("daemon_threads", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["daemon_threads"]), [float, int] - ) - - self.assertIn("gc", python_plugin["data"]["metrics"]) - self.assertIsInstance(python_plugin["data"]["metrics"]["gc"], dict) - self.assertIn("collect0", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["collect0"]), [float, int] - ) - self.assertIn("collect1", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["collect1"]), [float, int] - ) - self.assertIn("collect2", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["collect2"]), [float, int] - ) - self.assertIn("threshold0", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["threshold0"]), [float, int] - ) - self.assertIn("threshold1", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["threshold1"]), [float, int] - ) - self.assertIn("threshold2", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["threshold2"]), [float, int] - ) - - def test_prepare_payload_basics_disable_runtime_metrics(self): - os.environ["INSTANA_DISABLE_METRICS_COLLECTION"] = "TRUE" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(len(payload.keys()), 3) - self.assertIn("spans", payload) - self.assertIsInstance(payload["spans"], list) - self.assertEqual(len(payload["spans"]), 0) - self.assertIn("metrics", payload) - self.assertEqual(len(payload["metrics"].keys()), 1) - self.assertIn("plugins", payload["metrics"]) - self.assertIsInstance(payload["metrics"]["plugins"], list) - self.assertEqual(len(payload["metrics"]["plugins"]), 1) - - python_plugin = payload["metrics"]["plugins"][0] - self.assertEqual(python_plugin["name"], "com.instana.plugin.python") - self.assertEqual(python_plugin["entityId"], str(os.getpid())) - self.assertIn("data", python_plugin) - self.assertIn("snapshot", python_plugin["data"]) - self.assertIn("m", python_plugin["data"]["snapshot"]) - self.assertEqual("Manual", python_plugin["data"]["snapshot"]["m"]) - self.assertNotIn("metrics", python_plugin["data"]) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_with_python_packages( - self, mock_should_send_snapshot_data - ): - mock_should_send_snapshot_data.return_value = True - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("Manual", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertGreater(len(snapshot["versions"]), 5) - self.assertEqual(snapshot["versions"]["instana"], VERSION) - self.assertIn("wrapt", snapshot["versions"]) - self.assertIn("fysom", snapshot["versions"]) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_disabled_python_packages( - self, mock_should_send_snapshot_data - ): - mock_should_send_snapshot_data.return_value = True - os.environ["INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"] = "TRUE" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("Manual", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertEqual(len(snapshot["versions"]), 1) - self.assertEqual(snapshot["versions"]["instana"], VERSION) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_autowrapt(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - os.environ["AUTOWRAPT_BOOTSTRAP"] = "instana" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("Autowrapt", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertGreater(len(snapshot["versions"]), 5) - expected_packages = ("instana", "wrapt", "fysom") - for package in expected_packages: - self.assertIn( - package, - snapshot["versions"], - f"{package} not found in snapshot['versions']", - ) - self.assertEqual(snapshot["versions"]["instana"], VERSION) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_autotrace(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - - sys.path.append(self.webhook_sitedir_path) - - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("AutoTrace", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertGreater(len(snapshot["versions"]), 5) - expected_packages = ("instana", "wrapt", "fysom") - for package in expected_packages: - self.assertIn( - package, - snapshot["versions"], - f"{package} not found in snapshot['versions']", - ) - self.assertEqual(snapshot["versions"]["instana"], VERSION) diff --git a/tests/platforms/test_lambda.py b/tests/platforms/test_lambda.py deleted file mode 100644 index a5a25c09..00000000 --- a/tests/platforms/test_lambda.py +++ /dev/null @@ -1,762 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import json -import time -import logging -import unittest - -import wrapt - -from instana.tracer import InstanaTracer -from instana.agent.aws_lambda import AWSLambdaAgent -from instana.options import AWSLambdaOptions -from instana.recorder import StanRecorder -from instana import lambda_handler -from instana import get_lambda_handler_or_default -from instana.instrumentation.aws.lambda_inst import lambda_handler_with_instana -from instana.instrumentation.aws.triggers import read_http_query_params -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer -from instana.util.aws import normalize_aws_lambda_arn - - -# Mock Context object -class MockContext(dict): - def __init__(self, **kwargs): - super(MockContext, self).__init__(**kwargs) - self.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython:1" - self.function_name = "TestPython" - self.function_version = "1" - - -# This is the target handler that will be instrumented for these tests -def my_lambda_handler(event, context): - # print("target_handler called") - return { - 'statusCode': 200, - 'headers': {'Content-Type': 'application/json'}, - 'body': json.dumps({'site': 'pwpush.com', 'response': 204}) - } - -# We only want to monkey patch the test handler once so do it here -os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_lambda_handler" -module_name, function_name = get_lambda_handler_or_default() -wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) - -def my_errored_lambda_handler(event, context): - return { - 'statusCode': 500, - 'headers': {'Content-Type': 'application/json'}, - 'body': json.dumps({'site': 'wikipedia.org', 'response': 500}) - } - -os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_errored_lambda_handler" -module_name, function_name = get_lambda_handler_or_default() -wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) - -class TestLambda(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestLambda, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - self.pwd = os.path.dirname(os.path.realpath(__file__)) - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["AWS_EXECUTION_ENV"] = "AWS_Lambda_python_3.8" - os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_lambda_handler" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - self.context = MockContext() - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "AWS_EXECUTION_ENV" in os.environ: - os.environ.pop("AWS_EXECUTION_ENV") - if "LAMBDA_HANDLER" in os.environ: - os.environ.pop("LAMBDA_HANDLER") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_ENDPOINT_PROXY" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_PROXY") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_SERVICE_NAME" in os.environ: - os.environ.pop("INSTANA_SERVICE_NAME") - if "INSTANA_DEBUG" in os.environ: - os.environ.pop("INSTANA_DEBUG") - if "INSTANA_LOG_LEVEL" in os.environ: - os.environ.pop("INSTANA_LOG_LEVEL") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = AWSLambdaAgent() - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_invalid_options(self): - # None of the required env vars are available... - if "LAMBDA_HANDLER" in os.environ: - os.environ.pop("LAMBDA_HANDLER") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - - agent = AWSLambdaAgent() - self.assertFalse(agent._can_send) - self.assertIsNone(agent.collector) - - def test_secrets(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_has_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, AWSLambdaOptions)) - self.assertDictEqual(self.agent.options.endpoint_proxy, { }) - - def test_get_handler(self): - os.environ["LAMBDA_HANDLER"] = "tests.lambda_handler" - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests", handler_module) - self.assertEqual("lambda_handler", handler_function) - - def test_get_handler_with_multi_subpackages(self): - os.environ["LAMBDA_HANDLER"] = "tests.one.two.three.lambda_handler" - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests.one.two.three", handler_module) - self.assertEqual("lambda_handler", handler_function) - - def test_get_handler_with_space_in_it(self): - os.environ["LAMBDA_HANDLER"] = " tests.another_module.lambda_handler" - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests.another_module", handler_module) - self.assertEqual("lambda_handler", handler_function) - - os.environ["LAMBDA_HANDLER"] = "tests.another_module.lambda_handler " - handler_module, handler_function = get_lambda_handler_or_default() - - self.assertEqual("tests.another_module", handler_module) - self.assertEqual("lambda_handler", handler_function) - - def test_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertEqual(should_headers, self.agent.options.extra_http_headers) - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - self.assertDictEqual(self.agent.options.endpoint_proxy, { 'https': "http://myproxy.123" }) - - def test_custom_service_name(self): - os.environ['INSTANA_SERVICE_NAME'] = "Legion" - with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - os.environ.pop('INSTANA_SERVICE_NAME') - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('d5cb361b256413a9', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0901d8ae4fbf1529', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - 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']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - - self.assertEqual('Legion', span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual(200, span.data['http']['status']) - self.assertEqual('/path/to/resource', span.data['http']['url']) - self.assertEqual('/{proxy+}', span.data['http']['path_tpl']) - self.assertEqual("foo=['bar']", span.data['http']['params']) - - def test_api_gateway_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('d5cb361b256413a9', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0901d8ae4fbf1529', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - 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']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual(200, span.data['http']['status']) - self.assertEqual('/path/to/resource', span.data['http']['url']) - self.assertEqual('/{proxy+}', span.data['http']['path_tpl']) - self.assertEqual("foo=['bar']", span.data['http']['params']) - - def test_api_gateway_v2_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/api_gateway_v2_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - time.sleep(1) - payload = self.agent.collector.prepare_payload() - self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) - - self.assertEqual(200, result['statusCode']) - span = payload['spans'][0] - self.assertIsNone(span.ec) - self.assertIsNone(span.data['lambda']['error']) - self.assertEqual(200, span.data['http']['status']) - - - def test_api_gateway_v2_trigger_errored_tracing(self): - - with open(self.pwd + '/../data/lambda/api_gateway_v2_event.json', 'r') as json_file: - event = json.load(json_file) - - os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_errored_lambda_handler" - self.create_agent_and_setup_tracer() - - result = lambda_handler(event, self.context) - time.sleep(1) - payload = self.agent.collector.prepare_payload() - self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) - - self.assertEqual(500, result['statusCode']) - span = payload['spans'][0] - self.assertEqual(1, span.ec) - self.assertEqual('HTTP status 500', span.data['lambda']['error']) - self.assertEqual(500, span.data['http']['status']) - - - def test_application_lb_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('d5cb361b256413a9', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0901d8ae4fbf1529', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - 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']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual(200, span.data['http']['status']) - self.assertEqual('/path/to/resource', span.data['http']['url']) - self.assertEqual("foo=['bar']", span.data['http']['params']) - - def test_cloudwatch_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/cloudwatch_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - 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']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:cloudwatch.events', span.data['lambda']['trigger']) - self.assertEqual('cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', span.data["lambda"]["cw"]["events"]["id"]) - self.assertEqual(False, span.data["lambda"]["cw"]["events"]["more"]) - self.assertTrue(isinstance(span.data["lambda"]["cw"]["events"]["resources"], list)) - self.assertEqual(1, len(span.data["lambda"]["cw"]["events"]["resources"])) - self.assertEqual('arn:aws:events:eu-west-1:123456789012:rule/ExampleRule', - span.data["lambda"]["cw"]["events"]["resources"][0]) - - def test_cloudwatch_logs_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/cloudwatch_logs_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - 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']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:cloudwatch.logs', span.data['lambda']['trigger']) - self.assertFalse("decodingError" in span.data['lambda']['cw']['logs']) - self.assertEqual('testLogGroup', span.data['lambda']['cw']['logs']['group']) - self.assertEqual('testLogStream', span.data['lambda']['cw']['logs']['stream']) - self.assertEqual(None, span.data['lambda']['cw']['logs']['more']) - self.assertTrue(isinstance(span.data['lambda']['cw']['logs']['events'], list)) - self.assertEqual(2, len(span.data['lambda']['cw']['logs']['events'])) - self.assertEqual('[ERROR] First test message', span.data['lambda']['cw']['logs']['events'][0]) - self.assertEqual('[ERROR] Second test message', span.data['lambda']['cw']['logs']['events'][1]) - - def test_s3_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/s3_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - 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']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:s3', span.data['lambda']['trigger']) - self.assertTrue(isinstance(span.data["lambda"]["s3"]["events"], list)) - events = span.data["lambda"]["s3"]["events"] - self.assertEqual(1, len(events)) - event = events[0] - self.assertEqual('ObjectCreated:Put', event['event']) - self.assertEqual('example-bucket', event['bucket']) - self.assertEqual('test/key', event['object']) - - def test_sqs_trigger_tracing(self): - with open(self.pwd + '/../data/lambda/sqs_event.json', 'r') as json_file: - event = json.load(json_file) - - self.create_agent_and_setup_tracer() - - # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then - # figure out the original (the users') Lambda Handler and execute it. - # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] - result = lambda_handler(event, self.context) - - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - - time.sleep(1) - payload = self.agent.collector.prepare_payload() - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertIsNotNone(span.t) - self.assertIsNotNone(span.s) - self.assertIsNone(span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - 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']) - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:sqs', span.data['lambda']['trigger']) - self.assertTrue(isinstance(span.data["lambda"]["sqs"]["messages"], list)) - messages = span.data["lambda"]["sqs"]["messages"] - self.assertEqual(1, len(messages)) - message = messages[0] - self.assertEqual('arn:aws:sqs:us-west-1:123456789012:MyQueue', message['queue']) - - def test_read_query_params(self): - event = { "queryStringParameters": {"foo": "bar" }, - "multiValueQueryStringParameters": { "foo": ["bar"] } } - params = read_http_query_params(event) - self.assertEqual("foo=['bar']", params) - - def test_read_query_params_with_none_data(self): - event = { "queryStringParameters": None, - "multiValueQueryStringParameters": None } - params = read_http_query_params(event) - self.assertEqual("", params) - - def test_read_query_params_with_bad_event(self): - event = None - params = read_http_query_params(event) - self.assertEqual("", params) - - def test_arn_parsing(self): - ctx = MockContext() - - self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:1") - - # Without version should return a fully qualified ARN (with version) - ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython" - self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:1") - - # Fully qualified already with the '$LATEST' special tag - ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST" - self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST") - - def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.WARNING) - - def test_agent_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.ERROR) - - def __validate_result_and_payload_for_gateway_v2_trace(self, result, payload): - self.assertIsInstance(result, dict) - self.assertIn('headers', result) - self.assertIn('Server-Timing', result['headers']) - self.assertIn('statusCode', result) - - self.assertTrue("metrics" in payload) - self.assertTrue("spans" in payload) - self.assertEqual(2, len(payload.keys())) - - self.assertTrue(isinstance(payload['metrics']['plugins'], list)) - self.assertTrue(len(payload['metrics']['plugins']) == 1) - plugin_data = payload['metrics']['plugins'][0] - - self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) - - self.assertEqual(1, len(payload['spans'])) - - span = payload['spans'][0] - self.assertEqual('aws.lambda.entry', span.n) - self.assertEqual('0000000000001234', span.t) - self.assertIsNotNone(span.s) - self.assertEqual('0000000000004567', span.p) - self.assertIsNotNone(span.ts) - self.assertIsNotNone(span.d) - - server_timing_value = "intid;desc=%s" % span.t - self.assertEqual(result['headers']['Server-Timing'], server_timing_value) - - self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, - span.f) - - self.assertTrue(span.sy) - - - self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) - self.assertEqual(None, span.data['lambda']['alias']) - self.assertEqual('python', span.data['lambda']['runtime']) - self.assertEqual('TestPython', span.data['lambda']['functionName']) - self.assertEqual('1', span.data['lambda']['functionVersion']) - self.assertIsNone(span.data['service']) - - self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) - self.assertEqual('POST', span.data['http']['method']) - self.assertEqual('/my/path', span.data['http']['url']) - self.assertEqual('/my/{resource}', span.data['http']['path_tpl']) - self.assertEqual("secret=key&q=term", span.data['http']['params']) diff --git a/tests/propagators/test_base_propagator.py b/tests/propagators/test_base_propagator.py new file mode 100644 index 00000000..4c86a009 --- /dev/null +++ b/tests/propagators/test_base_propagator.py @@ -0,0 +1,93 @@ +import pytest + +from typing import Generator +from instana.propagators.base_propagator import BasePropagator +from unittest.mock import Mock + + +class TestBasePropagator: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.propagator = BasePropagator() + yield + self.propagator = None + + def test_extract_headers_dict(self) -> None: + carrier_as_a_dict = {"key": "value"} + assert carrier_as_a_dict == self.propagator.extract_headers_dict( + carrier_as_a_dict + ) + mocked_carrier = Mock() + mocked_carrier.__dict__ = carrier_as_a_dict + assert carrier_as_a_dict == self.propagator.extract_headers_dict(mocked_carrier) + wrong_carrier = "value" + assert self.propagator.extract_headers_dict(wrong_carrier) is None + + def test_get_ctx_level(self) -> None: + assert 3 == self.propagator._get_ctx_level("3,4") + assert 1 == self.propagator._get_ctx_level("wrong_data") + + def test_get_correlation_properties(self) -> None: + a, b = self.propagator._get_correlation_properties( + ",correlationType=3;correlationId=5;" + ) + assert a == "3" + assert b == "5" + assert "3", None == self.propagator._get_correlation_properties( # noqa: E711 + ",correlationType=3;" + ) + + def test_get_participating_trace_context(self, span_context) -> None: + traceparent, tracestate = self.propagator._get_participating_trace_context( + span_context + ) + assert traceparent == "00-1812338823475918251-6895521157646639861-01" + assert tracestate == "in=1812338823475918251;6895521157646639861" + + def test_extract_instana_headers(self) -> None: + dc = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": str.encode(",correlationType=3;correlationId=5;"), + "x-instana-synthetic": "1", + } + trace_id, span_id, level, synthetic = self.propagator.extract_instana_headers( + dc=dc + ) + assert trace_id == 123456789 + assert span_id == 12345 + assert level == ",correlationType=3;correlationId=5;" + assert synthetic + + def test_extract(self) -> None: + carrier = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": str.encode("3,correlationId=5;"), + "x-instana-synthetic": "1", + "traceparent": "00-1812338823475918251-6895521157646639861-01", + "tracestate": "in=1812338823475918251;6895521157646639861", + } + span_context = self.propagator.extract( + carrier=carrier, disable_w3c_trace_context=True + ) + assert span_context + span_context = self.propagator.extract(carrier=carrier) + span_context = self.propagator.extract( + carrier=None, disable_w3c_trace_context=True + ) + assert not span_context + carrier.pop("x-instana-t", None) + carrier.pop("x-instana-s", None) + span_context = self.propagator.extract(carrier=carrier) + assert span_context + carrier = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": "2,correlationType=3;correlationId=5;", + "x-instana-synthetic": "1", + "traceparent": "00-4bf92f3577b34da61234567899999999-1234567890888888-01", + "tracestate": "in=1812338823475918251;6895521157646639861", + } + span_context = self.propagator.extract(carrier=carrier) + assert span_context diff --git a/tests/test_sampling.py b/tests/test_sampling.py new file mode 100644 index 00000000..a3aa3136 --- /dev/null +++ b/tests/test_sampling.py @@ -0,0 +1,20 @@ +import pytest + +from typing import Generator +from instana.sampling import InstanaSampler, SamplingPolicy + + +class TestInstanaSampler: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.sampler = InstanaSampler() + yield + self.sampler = None + + def test_sampling_policy(self) -> None: + assert self.sampler._sampled == SamplingPolicy.DROP + assert self.sampler._sampled.name == "DROP" + assert self.sampler._sampled.value == 0 + + def test_sampler(self) -> None: + assert not self.sampler.sampled() diff --git a/tests/util/test_traceutils.py b/tests/util/test_traceutils.py new file mode 100644 index 00000000..f1817029 --- /dev/null +++ b/tests/util/test_traceutils.py @@ -0,0 +1,65 @@ +from unittest.mock import patch +import pytest + +from instana.tracer import InstanaTracer +from instana.util.traceutils import ( + extract_custom_headers, + get_active_tracer, + get_tracer_tuple, + tracing_is_off, +) +from instana.singletons import agent, tracer + + +class TestTraceUtils: + @pytest.fixture(autouse=True) + def _resource(self): + pass + + def test_extract_custom_headers(self, span) -> None: + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + request_headers = { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + } + extract_custom_headers(span, request_headers) + assert len(span.attributes) == 2 + assert span.attributes["http.header.X-Capture-This-Too"] == "this too" + assert span.attributes["http.header.X-Capture-That-Too"] == "that too" + + def test_get_activate_tracer(self) -> None: + assert not get_active_tracer() + + with tracer.start_as_current_span("test"): + response = get_active_tracer() + assert isinstance(response, InstanaTracer) + assert response == tracer + with patch( + "instana.span.span.InstanaSpan.is_recording", return_value=False + ): + assert not get_active_tracer() + + def test_get_tracer_tuple(self) -> None: + response = get_tracer_tuple() + assert response == (None, None, None) + + agent.options.allow_exit_as_root = True + response = get_tracer_tuple() + assert response == (tracer, None, None) + agent.options.allow_exit_as_root = False + + with tracer.start_as_current_span("test") as span: + response = get_tracer_tuple() + assert response == (tracer, span, span.name) + + def test_tracing_is_off(self) -> None: + response = tracing_is_off() + assert response + with tracer.start_as_current_span("test"): + response = tracing_is_off() + assert not response + + agent.options.allow_exit_as_root = True + response = tracing_is_off() + assert not response + agent.options.allow_exit_as_root = False From aca41fee12b89e323160650c089edd35ed0017df Mon Sep 17 00:00:00 2001 From: Cagri Yonca Date: Wed, 18 Sep 2024 12:31:39 +0300 Subject: [PATCH 172/172] unittest(redis): fix unittests and annotation --- src/instana/collector/base.py | 6 - src/instana/collector/utils.py | 2 +- src/instana/propagators/base_propagator.py | 194 ++++-- src/instana/sampling.py | 2 +- src/instana/span_context.py | 5 +- tests/agents/test_aws_eks_fargate.py | 120 ++++ tests/agents/test_aws_fargate.py | 125 ++++ tests/agents/test_aws_lambda.py | 762 +++++++++++++++++++++ tests/agents/test_google_cloud_run.py | 129 ++++ tests/collector/__init__.py | 0 tests/collector/test_fargate_collector.py | 242 +++++++ tests/collector/test_gcr_collector.py | 95 +++ tests/conftest.py | 13 +- 13 files changed, 1613 insertions(+), 82 deletions(-) create mode 100644 tests/agents/test_aws_eks_fargate.py create mode 100644 tests/agents/test_aws_fargate.py create mode 100644 tests/agents/test_aws_lambda.py create mode 100644 tests/agents/test_google_cloud_run.py create mode 100644 tests/collector/__init__.py create mode 100644 tests/collector/test_fargate_collector.py create mode 100644 tests/collector/test_gcr_collector.py diff --git a/src/instana/collector/base.py b/src/instana/collector/base.py index 67008e34..99abfa3f 100644 --- a/src/instana/collector/base.py +++ b/src/instana/collector/base.py @@ -137,12 +137,6 @@ def background_report(self): self.prepare_and_report_data() - if self.thread_shutdown.is_set(): - logger.debug( - "Thread shutdown signal is active: Shutting down reporting thread" - ) - return False - return True def prepare_and_report_data(self): diff --git a/src/instana/collector/utils.py b/src/instana/collector/utils.py index 7292cca4..74e3021f 100644 --- a/src/instana/collector/utils.py +++ b/src/instana/collector/utils.py @@ -23,6 +23,6 @@ def format_span( span.s = format_span_id(span.s) span.p = format_span_id(span.p) if span.p else None if isinstance(span.k, SpanKind): - span.k = span.k.value if not span.k is SpanKind.INTERNAL else 3 + span.k = span.k.value if span.k is not SpanKind.INTERNAL else 3 spans.append(span) return spans diff --git a/src/instana/propagators/base_propagator.py b/src/instana/propagators/base_propagator.py index 3e4fcdc8..cc49ec7e 100644 --- a/src/instana/propagators/base_propagator.py +++ b/src/instana/propagators/base_propagator.py @@ -32,40 +32,40 @@ class BasePropagator(object): - HEADER_KEY_T = 'X-INSTANA-T' - HEADER_KEY_S = 'X-INSTANA-S' - HEADER_KEY_L = 'X-INSTANA-L' - HEADER_KEY_SYNTHETIC = 'X-INSTANA-SYNTHETIC' + HEADER_KEY_T = "X-INSTANA-T" + HEADER_KEY_S = "X-INSTANA-S" + HEADER_KEY_L = "X-INSTANA-L" + HEADER_KEY_SYNTHETIC = "X-INSTANA-SYNTHETIC" HEADER_KEY_TRACEPARENT = "traceparent" HEADER_KEY_TRACESTATE = "tracestate" - 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' + 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_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' + 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" ALT_HEADER_KEY_TRACEPARENT = "http_traceparent" ALT_HEADER_KEY_TRACESTATE = "http_tracestate" # ByteArray variations - B_HEADER_KEY_T = b'x-instana-t' - B_HEADER_KEY_S = b'x-instana-s' - B_HEADER_KEY_L = b'x-instana-l' - B_HEADER_KEY_SYNTHETIC = b'x-instana-synthetic' - B_HEADER_SERVER_TIMING = b'server-timing' - B_HEADER_KEY_TRACEPARENT = b'traceparent' - B_HEADER_KEY_TRACESTATE = b'tracestate' - - B_ALT_LC_HEADER_KEY_T = b'http_x_instana_t' - B_ALT_LC_HEADER_KEY_S = b'http_x_instana_s' - B_ALT_LC_HEADER_KEY_L = b'http_x_instana_l' - B_ALT_LC_HEADER_KEY_SYNTHETIC = b'http_x_instana_synthetic' - B_ALT_HEADER_KEY_TRACEPARENT = b'http_traceparent' - B_ALT_HEADER_KEY_TRACESTATE = b'http_tracestate' + B_HEADER_KEY_T = b"x-instana-t" + B_HEADER_KEY_S = b"x-instana-s" + B_HEADER_KEY_L = b"x-instana-l" + B_HEADER_KEY_SYNTHETIC = b"x-instana-synthetic" + B_HEADER_SERVER_TIMING = b"server-timing" + B_HEADER_KEY_TRACEPARENT = b"traceparent" + B_HEADER_KEY_TRACESTATE = b"tracestate" + + B_ALT_LC_HEADER_KEY_T = b"http_x_instana_t" + B_ALT_LC_HEADER_KEY_S = b"http_x_instana_s" + B_ALT_LC_HEADER_KEY_L = b"http_x_instana_l" + B_ALT_LC_HEADER_KEY_SYNTHETIC = b"http_x_instana_synthetic" + B_ALT_HEADER_KEY_TRACEPARENT = b"http_traceparent" + B_ALT_HEADER_KEY_TRACESTATE = b"http_tracestate" def __init__(self): self._tp = Traceparent() @@ -88,7 +88,9 @@ def extract_headers_dict(carrier: CarrierT) -> Optional[Dict]: else: dc = dict(carrier) except Exception: - logger.debug(f"base_propagator extract_headers_dict: Couldn't convert - {carrier}") + logger.debug( + f"base_propagator extract_headers_dict: Couldn't convert - {carrier}" + ) return dc @@ -107,7 +109,7 @@ def _get_ctx_level(level: str) -> int: return ctx_level @staticmethod - def _get_correlation_properties(level:str): + def _get_correlation_properties(level: str): """ Get the correlation values if they are present. @@ -116,12 +118,16 @@ def _get_correlation_properties(level:str): """ correlation_type, correlation_id = [None] * 2 try: - correlation_type = level.split(",")[1].split("correlationType=")[1].split(";")[0] + correlation_type = ( + level.split(",")[1].split("correlationType=")[1].split(";")[0] + ) if "correlationId" in level: - correlation_id = level.split(",")[1].split("correlationId=")[1].split(";")[0] + correlation_id = ( + level.split(",")[1].split("correlationId=")[1].split(";")[0] + ) except Exception: logger.debug("extract instana correlation type/id error:", exc_info=True) - + return correlation_type, correlation_id def _get_participating_trace_context(self, span_context: SpanContext): @@ -137,7 +143,9 @@ def _get_participating_trace_context(self, span_context: SpanContext): tp_trace_id = span_context.trace_id traceparent = span_context.traceparent tracestate = span_context.tracestate - traceparent = self._tp.update_traceparent(traceparent, tp_trace_id, span_context.span_id, span_context.level) + traceparent = self._tp.update_traceparent( + traceparent, tp_trace_id, span_context.span_id, span_context.level + ) # In suppression mode do not update the tracestate and # do not add the 'in=' key-value pair to the incoming tracestate @@ -145,19 +153,21 @@ def _get_participating_trace_context(self, span_context: SpanContext): if span_context.suppression: return traceparent, tracestate - tracestate = self._ts.update_tracestate(tracestate, span_context.trace_id, span_context.span_id) + tracestate = self._ts.update_tracestate( + tracestate, span_context.trace_id, span_context.span_id + ) return traceparent, tracestate def __determine_span_context( - self, - trace_id: int, - span_id: int, - level: str, - synthetic: bool, - traceparent, - tracestate, - disable_w3c_trace_context: bool, - ) -> SpanContext: + self, + trace_id: int, + span_id: int, + level: str, + synthetic: bool, + traceparent, + tracestate, + disable_w3c_trace_context: bool, + ) -> SpanContext: """ This method determines the span context depending on a set of conditions being met Detailed description of the conditions can be found in the instana internal technical-documentation, @@ -173,7 +183,9 @@ def __determine_span_context( :return: SpanContext """ correlation = False - disable_traceparent = os.environ.get("INSTANA_DISABLE_W3C_TRACE_CORRELATION", "") + disable_traceparent = os.environ.get( + "INSTANA_DISABLE_W3C_TRACE_CORRELATION", "" + ) instana_ancestor = None if level and "correlationType" in level: @@ -183,7 +195,7 @@ def __determine_span_context( ( ctx_level, ctx_synthetic, - ctx_trace_parent, + ctx_trace_parent, ctx_instana_ancestor, ctx_long_trace_id, ctx_correlation_type, @@ -209,30 +221,39 @@ def __determine_span_context( # if len(trace_id) > 16: ctx_long_trace_id = trace_id - elif not disable_w3c_trace_context and traceparent and not trace_id and not span_id: - _, tp_trace_id, tp_parent_id, _ = self._tp.get_traceparent_fields(traceparent) + elif ( + not disable_w3c_trace_context + and traceparent + and not trace_id + and not span_id + ): + _, tp_trace_id, tp_parent_id, _ = self._tp.get_traceparent_fields( + traceparent + ) if tracestate and "in=" in tracestate: instana_ancestor = self._ts.get_instana_ancestor(tracestate) if disable_traceparent == "": - ctx_trace_id = tp_trace_id[-16:] - ctx_span_id = tp_parent_id + ctx_trace_id = int(tp_trace_id[-16:]) + ctx_span_id = int(tp_parent_id) ctx_synthetic = synthetic ctx_trace_parent = True ctx_instana_ancestor = instana_ancestor ctx_long_trace_id = tp_trace_id else: if instana_ancestor: - ctx_trace_id = instana_ancestor.t - ctx_span_id = instana_ancestor.p + ctx_trace_id = int(instana_ancestor.t) + ctx_span_id = int(instana_ancestor.p) ctx_synthetic = synthetic elif synthetic: ctx_synthetic = synthetic if correlation: - ctx_correlation_type, ctx_correlation_id = self._get_correlation_properties(level) + ctx_correlation_type, ctx_correlation_id = self._get_correlation_properties( + level + ) if traceparent: ctx_traceparent = traceparent @@ -253,8 +274,9 @@ def __determine_span_context( tracestate=ctx_tracestate, ) - - def extract_instana_headers(self, dc: Dict[str, Any]) -> Tuple[Optional[int], Optional[int], Optional[str], Optional[bool]]: + def extract_instana_headers( + self, dc: Dict[str, Any] + ) -> Tuple[Optional[int], Optional[int], Optional[str], Optional[bool]]: """ Search carrier for the *HEADER* keys and return the tracing key-values. @@ -265,27 +287,43 @@ def extract_instana_headers(self, dc: Dict[str, Any]) -> Tuple[Optional[int], Op # Headers can exist in the standard X-Instana-T/S format or the alternate HTTP_X_INSTANA_T/S style try: - trace_id = dc.get(self.LC_HEADER_KEY_T) or dc.get(self.ALT_LC_HEADER_KEY_T) or dc.get( - self.B_HEADER_KEY_T) or dc.get(self.B_ALT_LC_HEADER_KEY_T) + trace_id = ( + dc.get(self.LC_HEADER_KEY_T) + or dc.get(self.ALT_LC_HEADER_KEY_T) + or dc.get(self.B_HEADER_KEY_T) + or dc.get(self.B_ALT_LC_HEADER_KEY_T) + ) if trace_id: # trace_id = header_to_long_id(trace_id) trace_id = int(trace_id) - span_id = dc.get(self.LC_HEADER_KEY_S) or dc.get(self.ALT_LC_HEADER_KEY_S) or dc.get( - self.B_HEADER_KEY_S) or dc.get(self.B_ALT_LC_HEADER_KEY_S) + span_id = ( + dc.get(self.LC_HEADER_KEY_S) + or dc.get(self.ALT_LC_HEADER_KEY_S) + or dc.get(self.B_HEADER_KEY_S) + or dc.get(self.B_ALT_LC_HEADER_KEY_S) + ) if span_id: # span_id = header_to_id(span_id) span_id = int(span_id) - level = dc.get(self.LC_HEADER_KEY_L) or dc.get(self.ALT_LC_HEADER_KEY_L) or dc.get( - self.B_HEADER_KEY_L) or dc.get(self.B_ALT_LC_HEADER_KEY_L) + level = ( + dc.get(self.LC_HEADER_KEY_L) + or dc.get(self.ALT_LC_HEADER_KEY_L) + or dc.get(self.B_HEADER_KEY_L) + or dc.get(self.B_ALT_LC_HEADER_KEY_L) + ) if level and isinstance(level, bytes): level = level.decode("utf-8") - synthetic = dc.get(self.LC_HEADER_KEY_SYNTHETIC) or dc.get(self.ALT_LC_HEADER_KEY_SYNTHETIC) or dc.get( - self.B_HEADER_KEY_SYNTHETIC) or dc.get(self.B_ALT_LC_HEADER_KEY_SYNTHETIC) + synthetic = ( + dc.get(self.LC_HEADER_KEY_SYNTHETIC) + or dc.get(self.ALT_LC_HEADER_KEY_SYNTHETIC) + or dc.get(self.B_HEADER_KEY_SYNTHETIC) + or dc.get(self.B_ALT_LC_HEADER_KEY_SYNTHETIC) + ) if synthetic: - synthetic = synthetic in ['1', b'1'] + synthetic = synthetic in ["1", b"1"] except Exception: logger.debug("extract error:", exc_info=True) @@ -302,13 +340,21 @@ def __extract_w3c_trace_context_headers(self, dc): traceparent, tracestate = [None] * 2 try: - traceparent = dc.get(self.HEADER_KEY_TRACEPARENT) or dc.get(self.ALT_HEADER_KEY_TRACEPARENT) or dc.get( - self.B_HEADER_KEY_TRACEPARENT) or dc.get(self.B_ALT_HEADER_KEY_TRACEPARENT) + traceparent = ( + dc.get(self.HEADER_KEY_TRACEPARENT) + or dc.get(self.ALT_HEADER_KEY_TRACEPARENT) + or dc.get(self.B_HEADER_KEY_TRACEPARENT) + or dc.get(self.B_ALT_HEADER_KEY_TRACEPARENT) + ) if traceparent and isinstance(traceparent, bytes): traceparent = traceparent.decode("utf-8") - tracestate = dc.get(self.HEADER_KEY_TRACESTATE) or dc.get(self.ALT_HEADER_KEY_TRACESTATE) or dc.get( - self.B_HEADER_KEY_TRACESTATE) or dc.get(self.B_ALT_HEADER_KEY_TRACESTATE) + tracestate = ( + dc.get(self.HEADER_KEY_TRACESTATE) + or dc.get(self.ALT_HEADER_KEY_TRACESTATE) + or dc.get(self.B_HEADER_KEY_TRACESTATE) + or dc.get(self.B_ALT_HEADER_KEY_TRACESTATE) + ) if tracestate and isinstance(tracestate, bytes): tracestate = tracestate.decode("utf-8") @@ -317,10 +363,12 @@ def __extract_w3c_trace_context_headers(self, dc): return traceparent, tracestate - def extract(self, carrier: CarrierT, disable_w3c_trace_context: bool = False) -> Optional[SpanContext]: + def extract( + self, carrier: CarrierT, disable_w3c_trace_context: bool = False + ) -> Optional[SpanContext]: """ - This method overrides one of the Base classes as with the introduction - of W3C trace context for the HTTP requests more extracting steps and + This method overrides one of the Base classes as with the introduction + of W3C trace context for the HTTP requests more extracting steps and logic was required. :param disable_w3c_trace_context: @@ -334,9 +382,13 @@ def extract(self, carrier: CarrierT, disable_w3c_trace_context: bool = False) -> return None headers = {k.lower(): v for k, v in headers.items()} - trace_id, span_id, level, synthetic = self.extract_instana_headers(dc=headers) + trace_id, span_id, level, synthetic = self.extract_instana_headers( + dc=headers + ) if not disable_w3c_trace_context: - traceparent, tracestate = self.__extract_w3c_trace_context_headers(dc=headers) + traceparent, tracestate = self.__extract_w3c_trace_context_headers( + dc=headers + ) if traceparent: traceparent = self._tp.validate(traceparent) diff --git a/src/instana/sampling.py b/src/instana/sampling.py index 7f84f786..96d46d8e 100644 --- a/src/instana/sampling.py +++ b/src/instana/sampling.py @@ -31,7 +31,7 @@ def sampled(self) -> bool: Calling a span “sampled” can mean it was “sampled out” (dropped) or “sampled in” (recorded). """ - pass + pass # pragma: no cover class InstanaSampler(Sampler): diff --git a/src/instana/span_context.py b/src/instana/span_context.py index ac14e61d..d961fbf1 100644 --- a/src/instana/span_context.py +++ b/src/instana/span_context.py @@ -1,6 +1,7 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2019 +from opentelemetry.trace import SpanContext as OtelSpanContext import typing @@ -44,7 +45,9 @@ def __new__( tracestate=None, # temporary storage of the tracestate header **kwargs, ) -> "SpanContext": - instance = super().__new__(cls, trace_id, span_id, is_remote, trace_flags, trace_state) + instance = super().__new__( + cls, trace_id, span_id, is_remote, trace_flags, trace_state + ) return tuple.__new__( cls, ( diff --git a/tests/agents/test_aws_eks_fargate.py b/tests/agents/test_aws_eks_fargate.py new file mode 100644 index 00000000..9d6e2437 --- /dev/null +++ b/tests/agents/test_aws_eks_fargate.py @@ -0,0 +1,120 @@ +# (c) Copyright IBM Corp. 2024 + +import os +import logging +import unittest + +from instana.tracer import InstanaTracer +from instana.options import EKSFargateOptions +from instana.recorder import StanRecorder +from instana.agent.aws_eks_fargate import EKSFargateAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +class TestFargate(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestFargate, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["INSTANA_TRACER_ENVIRONMENT"] = "AWS_EKS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + + def tearDown(self): + """ Reset all environment variables of consequence """ + variable_names = ( + "INSTANA_TRACER_ENVIRONMENT", + "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", "INSTANA_ENDPOINT_PROXY", + "INSTANA_AGENT_KEY", "INSTANA_LOG_LEVEL", + "INSTANA_SECRETS", "INSTANA_DEBUG", "INSTANA_TAGS" + ) + + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = EKSFargateAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_has_options(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(isinstance(self.agent.options, EKSFargateOptions)) + + def test_missing_variables(self): + with self.assertLogs("instana", level=logging.WARN) as context: + os.environ.pop("INSTANA_ENDPOINT_URL") + agent = EKSFargateAgent() + self.assertFalse(agent.can_send()) + self.assertIsNone(agent.collector) + self.assertIn('environment variables not set', context.output[0]) + + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + with self.assertLogs("instana", level=logging.WARN) as context: + os.environ.pop("INSTANA_AGENT_KEY") + agent = EKSFargateAgent() + self.assertFalse(agent.can_send()) + self.assertIsNone(agent.collector) + self.assertIn('environment variables not set', context.output[0]) + + def test_default_secrets(self): + self.create_agent_and_setup_tracer() + self.assertIsNone(self.agent.options.secrets) + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertListEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) + + def test_custom_secrets(self): + os.environ["INSTANA_SECRETS"] = "equals:love,war,games" + self.create_agent_and_setup_tracer() + + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'equals') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertListEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) + + def test_default_tags(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent.options, 'tags')) + self.assertIsNone(self.agent.options.tags) + + def test_has_extra_http_headers(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) + + def test_agent_extra_http_headers(self): + os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" + self.create_agent_and_setup_tracer() + self.assertIsNotNone(self.agent.options.extra_http_headers) + should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] + self.assertListEqual(should_headers, self.agent.options.extra_http_headers) + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + self.assertEqual(self.agent.options.log_level, logging.WARNING) + + def test_agent_custom_log_level(self): + os.environ['INSTANA_LOG_LEVEL'] = "eRror" + self.create_agent_and_setup_tracer() + self.assertEqual(self.agent.options.log_level, logging.ERROR) + + def test_custom_proxy(self): + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.create_agent_and_setup_tracer() + self.assertDictEqual(self.agent.options.endpoint_proxy, {'https': "http://myproxy.123"}) diff --git a/tests/agents/test_aws_fargate.py b/tests/agents/test_aws_fargate.py new file mode 100644 index 00000000..7a50353a --- /dev/null +++ b/tests/agents/test_aws_fargate.py @@ -0,0 +1,125 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import os +import logging +import unittest + +from instana.tracer import InstanaTracer +from instana.options import AWSFargateOptions +from instana.recorder import StanRecorder +from instana.agent.aws_fargate import AWSFargateAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +class TestFargate(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestFargate, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + if "INSTANA_SECRETS" in os.environ: + os.environ.pop("INSTANA_SECRETS") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = AWSFargateAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_has_options(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(isinstance(self.agent.options, AWSFargateOptions)) + + def test_invalid_options(self): + # None of the required env vars are available... + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + + agent = AWSFargateAgent() + self.assertFalse(agent.can_send()) + self.assertIsNone(agent.collector) + + def test_default_secrets(self): + self.create_agent_and_setup_tracer() + self.assertIsNone(self.agent.options.secrets) + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertListEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) + + def test_custom_secrets(self): + os.environ["INSTANA_SECRETS"] = "equals:love,war,games" + self.create_agent_and_setup_tracer() + + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'equals') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertListEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) + + def test_default_tags(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent.options, 'tags')) + self.assertIsNone(self.agent.options.tags) + + def test_has_extra_http_headers(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) + + def test_agent_extra_http_headers(self): + os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" + self.create_agent_and_setup_tracer() + self.assertIsNotNone(self.agent.options.extra_http_headers) + should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] + self.assertListEqual(should_headers, self.agent.options.extra_http_headers) + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + self.assertEqual(self.agent.options.log_level, logging.WARNING) + + def test_agent_custom_log_level(self): + os.environ['INSTANA_LOG_LEVEL'] = "eRror" + self.create_agent_and_setup_tracer() + self.assertEqual(self.agent.options.log_level, logging.ERROR) + + def test_custom_proxy(self): + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.create_agent_and_setup_tracer() + self.assertDictEqual(self.agent.options.endpoint_proxy, {'https': "http://myproxy.123"}) diff --git a/tests/agents/test_aws_lambda.py b/tests/agents/test_aws_lambda.py new file mode 100644 index 00000000..a5a25c09 --- /dev/null +++ b/tests/agents/test_aws_lambda.py @@ -0,0 +1,762 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import os +import json +import time +import logging +import unittest + +import wrapt + +from instana.tracer import InstanaTracer +from instana.agent.aws_lambda import AWSLambdaAgent +from instana.options import AWSLambdaOptions +from instana.recorder import StanRecorder +from instana import lambda_handler +from instana import get_lambda_handler_or_default +from instana.instrumentation.aws.lambda_inst import lambda_handler_with_instana +from instana.instrumentation.aws.triggers import read_http_query_params +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer +from instana.util.aws import normalize_aws_lambda_arn + + +# Mock Context object +class MockContext(dict): + def __init__(self, **kwargs): + super(MockContext, self).__init__(**kwargs) + self.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython:1" + self.function_name = "TestPython" + self.function_version = "1" + + +# This is the target handler that will be instrumented for these tests +def my_lambda_handler(event, context): + # print("target_handler called") + return { + 'statusCode': 200, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps({'site': 'pwpush.com', 'response': 204}) + } + +# We only want to monkey patch the test handler once so do it here +os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_lambda_handler" +module_name, function_name = get_lambda_handler_or_default() +wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) + +def my_errored_lambda_handler(event, context): + return { + 'statusCode': 500, + 'headers': {'Content-Type': 'application/json'}, + 'body': json.dumps({'site': 'wikipedia.org', 'response': 500}) + } + +os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_errored_lambda_handler" +module_name, function_name = get_lambda_handler_or_default() +wrapt.wrap_function_wrapper(module_name, function_name, lambda_handler_with_instana) + +class TestLambda(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestLambda, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + self.pwd = os.path.dirname(os.path.realpath(__file__)) + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["AWS_EXECUTION_ENV"] = "AWS_Lambda_python_3.8" + os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_lambda_handler" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + self.context = MockContext() + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "LAMBDA_HANDLER" in os.environ: + os.environ.pop("LAMBDA_HANDLER") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_SERVICE_NAME" in os.environ: + os.environ.pop("INSTANA_SERVICE_NAME") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = AWSLambdaAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_invalid_options(self): + # None of the required env vars are available... + if "LAMBDA_HANDLER" in os.environ: + os.environ.pop("LAMBDA_HANDLER") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + + agent = AWSLambdaAgent() + self.assertFalse(agent._can_send) + self.assertIsNone(agent.collector) + + def test_secrets(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) + + def test_has_extra_http_headers(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) + + def test_has_options(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(isinstance(self.agent.options, AWSLambdaOptions)) + self.assertDictEqual(self.agent.options.endpoint_proxy, { }) + + def test_get_handler(self): + os.environ["LAMBDA_HANDLER"] = "tests.lambda_handler" + handler_module, handler_function = get_lambda_handler_or_default() + + self.assertEqual("tests", handler_module) + self.assertEqual("lambda_handler", handler_function) + + def test_get_handler_with_multi_subpackages(self): + os.environ["LAMBDA_HANDLER"] = "tests.one.two.three.lambda_handler" + handler_module, handler_function = get_lambda_handler_or_default() + + self.assertEqual("tests.one.two.three", handler_module) + self.assertEqual("lambda_handler", handler_function) + + def test_get_handler_with_space_in_it(self): + os.environ["LAMBDA_HANDLER"] = " tests.another_module.lambda_handler" + handler_module, handler_function = get_lambda_handler_or_default() + + self.assertEqual("tests.another_module", handler_module) + self.assertEqual("lambda_handler", handler_function) + + os.environ["LAMBDA_HANDLER"] = "tests.another_module.lambda_handler " + handler_module, handler_function = get_lambda_handler_or_default() + + self.assertEqual("tests.another_module", handler_module) + self.assertEqual("lambda_handler", handler_function) + + def test_agent_extra_http_headers(self): + os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" + self.create_agent_and_setup_tracer() + self.assertIsNotNone(self.agent.options.extra_http_headers) + should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] + self.assertEqual(should_headers, self.agent.options.extra_http_headers) + + def test_custom_proxy(self): + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.create_agent_and_setup_tracer() + self.assertDictEqual(self.agent.options.endpoint_proxy, { 'https': "http://myproxy.123" }) + + def test_custom_service_name(self): + os.environ['INSTANA_SERVICE_NAME'] = "Legion" + with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + os.environ.pop('INSTANA_SERVICE_NAME') + + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertEqual('d5cb361b256413a9', span.t) + self.assertIsNotNone(span.s) + self.assertEqual('0901d8ae4fbf1529', span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + 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']) + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + + self.assertEqual('Legion', span.data['service']) + + self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) + self.assertEqual('POST', span.data['http']['method']) + self.assertEqual(200, span.data['http']['status']) + self.assertEqual('/path/to/resource', span.data['http']['url']) + self.assertEqual('/{proxy+}', span.data['http']['path_tpl']) + self.assertEqual("foo=['bar']", span.data['http']['params']) + + def test_api_gateway_trigger_tracing(self): + with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertEqual('d5cb361b256413a9', span.t) + self.assertIsNotNone(span.s) + self.assertEqual('0901d8ae4fbf1529', span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + 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']) + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + self.assertIsNone(span.data['service']) + + self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) + self.assertEqual('POST', span.data['http']['method']) + self.assertEqual(200, span.data['http']['status']) + self.assertEqual('/path/to/resource', span.data['http']['url']) + self.assertEqual('/{proxy+}', span.data['http']['path_tpl']) + self.assertEqual("foo=['bar']", span.data['http']['params']) + + def test_api_gateway_v2_trigger_tracing(self): + with open(self.pwd + '/../data/lambda/api_gateway_v2_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + time.sleep(1) + payload = self.agent.collector.prepare_payload() + self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) + + self.assertEqual(200, result['statusCode']) + span = payload['spans'][0] + self.assertIsNone(span.ec) + self.assertIsNone(span.data['lambda']['error']) + self.assertEqual(200, span.data['http']['status']) + + + def test_api_gateway_v2_trigger_errored_tracing(self): + + with open(self.pwd + '/../data/lambda/api_gateway_v2_event.json', 'r') as json_file: + event = json.load(json_file) + + os.environ["LAMBDA_HANDLER"] = "tests.platforms.test_lambda.my_errored_lambda_handler" + self.create_agent_and_setup_tracer() + + result = lambda_handler(event, self.context) + time.sleep(1) + payload = self.agent.collector.prepare_payload() + self.__validate_result_and_payload_for_gateway_v2_trace(result, payload) + + self.assertEqual(500, result['statusCode']) + span = payload['spans'][0] + self.assertEqual(1, span.ec) + self.assertEqual('HTTP status 500', span.data['lambda']['error']) + self.assertEqual(500, span.data['http']['status']) + + + def test_application_lb_trigger_tracing(self): + with open(self.pwd + '/../data/lambda/api_gateway_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertEqual('d5cb361b256413a9', span.t) + self.assertIsNotNone(span.s) + self.assertEqual('0901d8ae4fbf1529', span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + 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']) + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + self.assertIsNone(span.data['service']) + + self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) + self.assertEqual('POST', span.data['http']['method']) + self.assertEqual(200, span.data['http']['status']) + self.assertEqual('/path/to/resource', span.data['http']['url']) + self.assertEqual("foo=['bar']", span.data['http']['params']) + + def test_cloudwatch_trigger_tracing(self): + with open(self.pwd + '/../data/lambda/cloudwatch_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertIsNotNone(span.t) + self.assertIsNotNone(span.s) + self.assertIsNone(span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + 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']) + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + self.assertIsNone(span.data['service']) + + self.assertEqual('aws:cloudwatch.events', span.data['lambda']['trigger']) + self.assertEqual('cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', span.data["lambda"]["cw"]["events"]["id"]) + self.assertEqual(False, span.data["lambda"]["cw"]["events"]["more"]) + self.assertTrue(isinstance(span.data["lambda"]["cw"]["events"]["resources"], list)) + self.assertEqual(1, len(span.data["lambda"]["cw"]["events"]["resources"])) + self.assertEqual('arn:aws:events:eu-west-1:123456789012:rule/ExampleRule', + span.data["lambda"]["cw"]["events"]["resources"][0]) + + def test_cloudwatch_logs_trigger_tracing(self): + with open(self.pwd + '/../data/lambda/cloudwatch_logs_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertIsNotNone(span.t) + self.assertIsNotNone(span.s) + self.assertIsNone(span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + 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']) + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + self.assertIsNone(span.data['service']) + + self.assertEqual('aws:cloudwatch.logs', span.data['lambda']['trigger']) + self.assertFalse("decodingError" in span.data['lambda']['cw']['logs']) + self.assertEqual('testLogGroup', span.data['lambda']['cw']['logs']['group']) + self.assertEqual('testLogStream', span.data['lambda']['cw']['logs']['stream']) + self.assertEqual(None, span.data['lambda']['cw']['logs']['more']) + self.assertTrue(isinstance(span.data['lambda']['cw']['logs']['events'], list)) + self.assertEqual(2, len(span.data['lambda']['cw']['logs']['events'])) + self.assertEqual('[ERROR] First test message', span.data['lambda']['cw']['logs']['events'][0]) + self.assertEqual('[ERROR] Second test message', span.data['lambda']['cw']['logs']['events'][1]) + + def test_s3_trigger_tracing(self): + with open(self.pwd + '/../data/lambda/s3_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertIsNotNone(span.t) + self.assertIsNotNone(span.s) + self.assertIsNone(span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + 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']) + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + self.assertIsNone(span.data['service']) + + self.assertEqual('aws:s3', span.data['lambda']['trigger']) + self.assertTrue(isinstance(span.data["lambda"]["s3"]["events"], list)) + events = span.data["lambda"]["s3"]["events"] + self.assertEqual(1, len(events)) + event = events[0] + self.assertEqual('ObjectCreated:Put', event['event']) + self.assertEqual('example-bucket', event['bucket']) + self.assertEqual('test/key', event['object']) + + def test_sqs_trigger_tracing(self): + with open(self.pwd + '/../data/lambda/sqs_event.json', 'r') as json_file: + event = json.load(json_file) + + self.create_agent_and_setup_tracer() + + # Call the Instana Lambda Handler as we do in the real world. It will initiate tracing and then + # figure out the original (the users') Lambda Handler and execute it. + # The original Lambda handler is set in os.environ["LAMBDA_HANDLER"] + result = lambda_handler(event, self.context) + + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + + time.sleep(1) + payload = self.agent.collector.prepare_payload() + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertIsNotNone(span.t) + self.assertIsNotNone(span.s) + self.assertIsNone(span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + 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']) + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + self.assertIsNone(span.data['service']) + + self.assertEqual('aws:sqs', span.data['lambda']['trigger']) + self.assertTrue(isinstance(span.data["lambda"]["sqs"]["messages"], list)) + messages = span.data["lambda"]["sqs"]["messages"] + self.assertEqual(1, len(messages)) + message = messages[0] + self.assertEqual('arn:aws:sqs:us-west-1:123456789012:MyQueue', message['queue']) + + def test_read_query_params(self): + event = { "queryStringParameters": {"foo": "bar" }, + "multiValueQueryStringParameters": { "foo": ["bar"] } } + params = read_http_query_params(event) + self.assertEqual("foo=['bar']", params) + + def test_read_query_params_with_none_data(self): + event = { "queryStringParameters": None, + "multiValueQueryStringParameters": None } + params = read_http_query_params(event) + self.assertEqual("", params) + + def test_read_query_params_with_bad_event(self): + event = None + params = read_http_query_params(event) + self.assertEqual("", params) + + def test_arn_parsing(self): + ctx = MockContext() + + self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:1") + + # Without version should return a fully qualified ARN (with version) + ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython" + self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:1") + + # Fully qualified already with the '$LATEST' special tag + ctx.invoked_function_arn = "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST" + self.assertEqual(normalize_aws_lambda_arn(ctx), "arn:aws:lambda:us-east-2:12345:function:TestPython:$LATEST") + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + self.assertEqual(self.agent.options.log_level, logging.WARNING) + + def test_agent_custom_log_level(self): + os.environ['INSTANA_LOG_LEVEL'] = "eRror" + self.create_agent_and_setup_tracer() + self.assertEqual(self.agent.options.log_level, logging.ERROR) + + def __validate_result_and_payload_for_gateway_v2_trace(self, result, payload): + self.assertIsInstance(result, dict) + self.assertIn('headers', result) + self.assertIn('Server-Timing', result['headers']) + self.assertIn('statusCode', result) + + self.assertTrue("metrics" in payload) + self.assertTrue("spans" in payload) + self.assertEqual(2, len(payload.keys())) + + self.assertTrue(isinstance(payload['metrics']['plugins'], list)) + self.assertTrue(len(payload['metrics']['plugins']) == 1) + plugin_data = payload['metrics']['plugins'][0] + + self.assertEqual('com.instana.plugin.aws.lambda', plugin_data['name']) + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', plugin_data['entityId']) + + self.assertEqual(1, len(payload['spans'])) + + span = payload['spans'][0] + self.assertEqual('aws.lambda.entry', span.n) + self.assertEqual('0000000000001234', span.t) + self.assertIsNotNone(span.s) + self.assertEqual('0000000000004567', span.p) + self.assertIsNotNone(span.ts) + self.assertIsNotNone(span.d) + + server_timing_value = "intid;desc=%s" % span.t + self.assertEqual(result['headers']['Server-Timing'], server_timing_value) + + self.assertEqual({'hl': True, 'cp': 'aws', 'e': 'arn:aws:lambda:us-east-2:12345:function:TestPython:1'}, + span.f) + + self.assertTrue(span.sy) + + + self.assertEqual('arn:aws:lambda:us-east-2:12345:function:TestPython:1', span.data['lambda']['arn']) + self.assertEqual(None, span.data['lambda']['alias']) + self.assertEqual('python', span.data['lambda']['runtime']) + self.assertEqual('TestPython', span.data['lambda']['functionName']) + self.assertEqual('1', span.data['lambda']['functionVersion']) + self.assertIsNone(span.data['service']) + + self.assertEqual('aws:api.gateway', span.data['lambda']['trigger']) + self.assertEqual('POST', span.data['http']['method']) + self.assertEqual('/my/path', span.data['http']['url']) + self.assertEqual('/my/{resource}', span.data['http']['path_tpl']) + self.assertEqual("secret=key&q=term", span.data['http']['params']) diff --git a/tests/agents/test_google_cloud_run.py b/tests/agents/test_google_cloud_run.py new file mode 100644 index 00000000..8b086a70 --- /dev/null +++ b/tests/agents/test_google_cloud_run.py @@ -0,0 +1,129 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + +import os +import logging +import unittest + +from instana.tracer import InstanaTracer +from instana.options import GCROptions +from instana.recorder import StanRecorder +from instana.agent.google_cloud_run import GCRAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +class TestGCR(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestGCR, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["K_SERVICE"] = "service" + os.environ["K_CONFIGURATION"] = "configuration" + os.environ["K_REVISION"] = "revision" + os.environ["PORT"] = "port" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "K_SERVICE" in os.environ: + os.environ.pop("K_SERVICE") + if "K_CONFIGURATION" in os.environ: + os.environ.pop("K_CONFIGURATION") + if "K_REVISION" in os.environ: + os.environ.pop("K_REVISION") + if "PORT" in os.environ: + os.environ.pop("PORT") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + if "INSTANA_SECRETS" in os.environ: + os.environ.pop("INSTANA_SECRETS") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = GCRAgent(service="service", configuration="configuration", revision="revision") + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_has_options(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(isinstance(self.agent.options, GCROptions)) + + def test_invalid_options(self): + # None of the required env vars are available... + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + + agent = GCRAgent(service="service", configuration="configuration", revision="revision") + self.assertFalse(agent.can_send()) + self.assertIsNone(agent.collector) + + def test_default_secrets(self): + self.create_agent_and_setup_tracer() + self.assertIsNone(self.agent.options.secrets) + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) + + def test_custom_secrets(self): + os.environ["INSTANA_SECRETS"] = "equals:love,war,games" + self.create_agent_and_setup_tracer() + + self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) + self.assertEqual(self.agent.options.secrets_matcher, 'equals') + self.assertTrue(hasattr(self.agent.options, 'secrets_list')) + self.assertEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) + + def test_has_extra_http_headers(self): + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent, 'options')) + self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) + + def test_agent_extra_http_headers(self): + os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" + self.create_agent_and_setup_tracer() + self.assertIsNotNone(self.agent.options.extra_http_headers) + should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] + self.assertEqual(should_headers, self.agent.options.extra_http_headers) + + def test_agent_default_log_level(self): + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.WARNING + + def test_agent_custom_log_level(self): + os.environ['INSTANA_LOG_LEVEL'] = "eRror" + self.create_agent_and_setup_tracer() + assert self.agent.options.log_level == logging.ERROR + + def test_custom_proxy(self): + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.create_agent_and_setup_tracer() + assert self.agent.options.endpoint_proxy == {'https': "http://myproxy.123"} diff --git a/tests/collector/__init__.py b/tests/collector/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/collector/test_fargate_collector.py b/tests/collector/test_fargate_collector.py new file mode 100644 index 00000000..361d14e8 --- /dev/null +++ b/tests/collector/test_fargate_collector.py @@ -0,0 +1,242 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import os +import json +import unittest + +from instana.tracer import InstanaTracer +from instana.recorder import StanRecorder +from instana.agent.aws_fargate import AWSFargateAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +def get_docker_plugin(plugins): + """ + Given a list of plugins, find and return the docker plugin that we're interested in from the mock data + """ + docker_plugin = None + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.docker" and plugin["entityId"] == "arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82::docker-ssh-aws-fargate": + docker_plugin = plugin + return docker_plugin + + +class TestFargateCollector(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestFargateCollector, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + self.pwd = os.path.dirname(os.path.realpath(__file__)) + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["AWS_EXECUTION_ENV"] = "AWS_ECS_FARGATE" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "AWS_EXECUTION_ENV" in os.environ: + os.environ.pop("AWS_EXECUTION_ENV") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = AWSFargateAgent() + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + # Manually set the ECS Metadata API results on the collector + with open(self.pwd + '/../data/fargate/1.3.0/root_metadata.json', 'r') as json_file: + self.agent.collector.root_metadata = json.load(json_file) + with open(self.pwd + '/../data/fargate/1.3.0/task_metadata.json', 'r') as json_file: + self.agent.collector.task_metadata = json.load(json_file) + with open(self.pwd + '/../data/fargate/1.3.0/stats_metadata.json', 'r') as json_file: + self.agent.collector.stats_metadata = json.load(json_file) + with open(self.pwd + '/../data/fargate/1.3.0/task_stats_metadata.json', 'r') as json_file: + self.agent.collector.task_stats_metadata = json.load(json_file) + + def test_prepare_payload_basics(self): + self.create_agent_and_setup_tracer() + + payload = self.agent.collector.prepare_payload() + self.assertTrue(payload) + + self.assertEqual(2, len(payload.keys())) + self.assertIn('spans',payload) + self.assertIsInstance(payload['spans'], list) + self.assertEqual(0, len(payload['spans'])) + self.assertIn('metrics', payload) + self.assertEqual(1, len(payload['metrics'].keys())) + self.assertIn('plugins', payload['metrics']) + self.assertIsInstance(payload['metrics']['plugins'], list) + self.assertEqual(7, len(payload['metrics']['plugins'])) + + plugins = payload['metrics']['plugins'] + for plugin in plugins: + # print("%s - %s" % (plugin["name"], plugin["entityId"])) + self.assertIn('name', plugin) + self.assertIn('entityId', plugin) + self.assertIn('data', plugin) + + def test_docker_plugin_snapshot_data(self): + self.create_agent_and_setup_tracer() + + first_payload = self.agent.collector.prepare_payload() + second_payload = self.agent.collector.prepare_payload() + + self.assertTrue(first_payload) + self.assertTrue(second_payload) + + plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) + plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) + + self.assertTrue(plugin_first_report) + self.assertIn("data", plugin_first_report) + + # First report should have snapshot data + data = plugin_first_report["data"] + self.assertEqual(data["Id"], "63dc7ac9f3130bba35c785ed90ff12aad82087b5c5a0a45a922c45a64128eb45") + self.assertEqual(data["Created"], "2020-07-27T12:14:12.583114444Z") + self.assertEqual(data["Started"], "2020-07-27T12:14:13.545410186Z") + self.assertEqual(data["Image"], "410797082306.dkr.ecr.us-east-2.amazonaws.com/fargate-docker-ssh:latest") + self.assertEqual(data["Labels"], {'com.amazonaws.ecs.cluster': 'arn:aws:ecs:us-east-2:410797082306:cluster/lombardo-ssh-cluster', 'com.amazonaws.ecs.container-name': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-arn': 'arn:aws:ecs:us-east-2:410797082306:task/2d60afb1-e7fd-4761-9430-a375293a9b82', 'com.amazonaws.ecs.task-definition-family': 'docker-ssh-aws-fargate', 'com.amazonaws.ecs.task-definition-version': '1'}) + self.assertIsNone(data["Ports"]) + + # Second report should have no snapshot data + self.assertTrue(plugin_second_report) + self.assertIn("data", plugin_second_report) + data = plugin_second_report["data"] + self.assertIn("Id", data) + self.assertNotIn("Created", data) + self.assertNotIn("Started", data) + self.assertNotIn("Image", data) + self.assertNotIn("Labels", data) + self.assertNotIn("Ports", data) + + def test_docker_plugin_metrics(self): + self.create_agent_and_setup_tracer() + + first_payload = self.agent.collector.prepare_payload() + second_payload = self.agent.collector.prepare_payload() + + self.assertTrue(first_payload) + self.assertTrue(second_payload) + + plugin_first_report = get_docker_plugin(first_payload['metrics']['plugins']) + self.assertTrue(plugin_first_report) + self.assertIn("data", plugin_first_report) + + plugin_second_report = get_docker_plugin(second_payload['metrics']['plugins']) + self.assertTrue(plugin_second_report) + self.assertIn("data", plugin_second_report) + + # First report should report all metrics + data = plugin_first_report.get("data", None) + self.assertTrue(data) + self.assertNotIn("network", data) + + cpu = data.get("cpu", None) + self.assertTrue(cpu) + self.assertEqual(cpu["total_usage"], 0.011033) + self.assertEqual(cpu["user_usage"], 0.009918) + self.assertEqual(cpu["system_usage"], 0.00089) + self.assertEqual(cpu["throttling_count"], 0) + self.assertEqual(cpu["throttling_time"], 0) + + memory = data.get("memory", None) + self.assertTrue(memory) + self.assertEqual(memory["active_anon"], 78721024) + self.assertEqual(memory["active_file"], 18501632) + self.assertEqual(memory["inactive_anon"], 0) + self.assertEqual(memory["inactive_file"], 71684096) + self.assertEqual(memory["total_cache"], 90185728) + self.assertEqual(memory["total_rss"], 78721024) + self.assertEqual(memory["usage"], 193769472) + self.assertEqual(memory["max_usage"], 195305472) + self.assertEqual(memory["limit"], 536870912) + + blkio = data.get("blkio", None) + self.assertTrue(blkio) + self.assertEqual(blkio["blk_read"], 0) + self.assertEqual(blkio["blk_write"], 128352256) + + # Second report should report the delta (in the test case, nothing) + data = plugin_second_report["data"] + self.assertIn("cpu", data) + self.assertEqual(len(data["cpu"]), 0) + self.assertIn("memory", data) + self.assertEqual(len(data["memory"]), 0) + self.assertIn("blkio", data) + self.assertEqual(len(data["blkio"]), 1) + self.assertEqual(data["blkio"]['blk_write'], 0) + self.assertNotIn('blk_read', data["blkio"]) + + def test_no_instana_zone(self): + self.create_agent_and_setup_tracer() + self.assertIsNone(self.agent.options.zone) + + def test_instana_zone(self): + os.environ["INSTANA_ZONE"] = "YellowDog" + self.create_agent_and_setup_tracer() + + self.assertEqual(self.agent.options.zone, "YellowDog") + + payload = self.agent.collector.prepare_payload() + self.assertTrue(payload) + + plugins = payload['metrics']['plugins'] + self.assertIsInstance(plugins, list) + + task_plugin = None + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.aws.ecs.task": + task_plugin = plugin + + self.assertTrue(task_plugin) + self.assertIn("data", task_plugin) + self.assertIn("instanaZone", task_plugin["data"]) + self.assertEqual(task_plugin["data"]["instanaZone"], "YellowDog") + + def test_custom_tags(self): + os.environ["INSTANA_TAGS"] = "love,war=1,games" + self.create_agent_and_setup_tracer() + self.assertTrue(hasattr(self.agent.options, 'tags')) + self.assertDictEqual(self.agent.options.tags, {"love": None, "war": "1", "games": None}) + + payload = self.agent.collector.prepare_payload() + + self.assertTrue(payload) + task_plugin = None + plugins = payload['metrics']['plugins'] + for plugin in plugins: + if plugin["name"] == "com.instana.plugin.aws.ecs.task": + task_plugin = plugin + self.assertTrue(task_plugin) + self.assertIn("tags", task_plugin["data"]) + tags = task_plugin["data"]["tags"] + self.assertEqual(tags["war"], "1") + self.assertIsNone(tags["love"]) + self.assertIsNone(tags["games"]) diff --git a/tests/collector/test_gcr_collector.py b/tests/collector/test_gcr_collector.py new file mode 100644 index 00000000..39c7e886 --- /dev/null +++ b/tests/collector/test_gcr_collector.py @@ -0,0 +1,95 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + +import os +import json +import unittest + +import requests_mock + +from instana.tracer import InstanaTracer +from instana.recorder import StanRecorder +from instana.agent.google_cloud_run import GCRAgent +from instana.singletons import get_agent, set_agent, get_tracer, set_tracer + + +class TestGCRCollector(unittest.TestCase): + def __init__(self, methodName='runTest'): + super(TestGCRCollector, self).__init__(methodName) + self.agent = None + self.span_recorder = None + self.tracer = None + self.pwd = os.path.dirname(os.path.realpath(__file__)) + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + def setUp(self): + os.environ["PORT"] = "port" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + def tearDown(self): + """ Reset all environment variables of consequence """ + if "PORT" in os.environ: + os.environ.pop("PORT") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_ZONE" in os.environ: + os.environ.pop("INSTANA_ZONE") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer(self): + self.agent = GCRAgent(service="service", configuration="configuration", revision="revision") + self.span_recorder = StanRecorder(self.agent) + self.tracer = InstanaTracer(recorder=self.span_recorder) + set_agent(self.agent) + set_tracer(self.tracer) + + # Manually set the Instance and Project Metadata API results on the collector + with open(self.pwd + '/../data/gcr/instance_metadata.json', 'r') as json_file: + self.agent.collector.instance_metadata = json.load(json_file) + with open(self.pwd + '/../data/gcr/project_metadata.json', 'r') as json_file: + self.agent.collector.project_metadata = json.load(json_file) + + @requests_mock.Mocker() + def test_prepare_payload_basics(self, m): + self.create_agent_and_setup_tracer() + m.get("http://metadata.google.internal/computeMetadata/v1/project/?recursive=true", + headers={"Metadata-Flavor": "Google"}, json=self.agent.collector.project_metadata) + + m.get("http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true", + headers={"Metadata-Flavor": "Google"}, json=self.agent.collector.instance_metadata) + + payload = self.agent.collector.prepare_payload() + assert (payload) + + assert (len(payload.keys()) == 2) + assert ('spans' in payload) + assert (isinstance(payload['spans'], list)) + assert (len(payload['spans']) == 0) + assert ('metrics' in payload) + assert (len(payload['metrics'].keys()) == 1) + assert ('plugins' in payload['metrics']) + assert (isinstance(payload['metrics']['plugins'], list)) + assert (len(payload['metrics']['plugins']) == 2) + + plugins = payload['metrics']['plugins'] + for plugin in plugins: + # print("%s - %s" % (plugin["name"], plugin["entityId"])) + assert ('name' in plugin) + assert ('entityId' in plugin) + assert ('data' in plugin) diff --git a/tests/conftest.py b/tests/conftest.py index 3d35e568..25079b90 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,8 +26,8 @@ "*autoprofile*", # "*clients*", # "*frameworks*", - "*platforms*", - "*propagators*", + # "*platforms*", + # "*propagators*", "*w3c_trace_context*", ] @@ -40,6 +40,15 @@ collect_ignore_glob.append("*frameworks/test_grpcio*") collect_ignore_glob.append("*frameworks/test_tornado*") +collect_ignore_glob.append("*propagators/test_binary*") +collect_ignore_glob.append("*propagators/test_http*") + +collect_ignore_glob.append("*agents/test_aws*") +collect_ignore_glob.append("*agents/test_google*") +collect_ignore_glob.append("*collector/test_gcr*") +collect_ignore_glob.append("*collector/test_eks*") +collect_ignore_glob.append("*collector/test_fargate*") + # # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will # # be run explicitly. (So always exclude them here) if not os.environ.get("CASSANDRA_TEST"):