From f642e6ccbc97de582e351ffd256377e0a0a08ea0 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Tue, 18 Aug 2026 15:39:27 -0700 Subject: [PATCH 1/4] Rm tests,scripts dir from exclude --- .flake8 | 2 -- pyproject.toml | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.flake8 b/.flake8 index 3457b6a20..14d01746d 100644 --- a/.flake8 +++ b/.flake8 @@ -12,5 +12,3 @@ exclude = .tox __pycache__ build - scripts - tests diff --git a/pyproject.toml b/pyproject.toml index 0b40c22f4..52ac27c5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,9 +89,7 @@ exclude = ''' ( /( # generated files .tox| - build| - scripts| - tests + build )/ ) ''' @@ -101,8 +99,6 @@ line-length = 79 exclude = [ ".tox", "build", - "scripts", - "tests", "__pycache__", ] From 761fa117d6f578d244e306d98fcafe0b44811c0c Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 19 Aug 2026 11:54:40 -0700 Subject: [PATCH 2/4] Ruff all tests and scripts --- lambda/tests/test_instrumentation.py | 10 +- scripts/lint_and_format.py | 6 +- tests/docker/install/app.py | 6 +- tests/docker/install/client.py | 24 +- tests/integration/__init__.py | 2 +- .../integration/test_base_sw_headers_attrs.py | 32 +- tests/integration/test_scenario_1.py | 79 +- tests/integration/test_scenario_4.py | 167 +- tests/integration/test_scenario_8.py | 642 ++++--- tests/integration/test_service_instance_id.py | 35 +- tests/integration/test_service_name.py | 23 +- tests/integration/test_signed_tt.py | 678 ++++--- tests/integration/test_unsigned_tt.py | 446 +++-- .../test_xtraceoptions_validation.py | 415 +++-- tests/unit/__init__.py | 2 +- tests/unit/test_api.py | 145 +- tests/unit/test_apm_config/conftest.py | 24 + .../unit/test_apm_config/fixtures/cnf_dict.py | 5 +- .../unit/test_apm_config/fixtures/cnf_file.py | 2 + .../unit/test_apm_config/fixtures/env_vars.py | 13 +- tests/unit/test_apm_config/test_apm_config.py | 477 +++-- .../test_apm_config_agent_enabled.py | 260 +-- .../test_apm_config_calculate_collector.py | 55 +- ...st_apm_config_calculate_metrics_enabled.py | 73 +- .../test_apm_config_cnf_file.py | 155 +- .../test_apm_config_service_name.py | 83 +- .../test_apm_config_transaction_filters.py | 109 +- tests/unit/test_apm_logging.py | 31 +- tests/unit/test_apm_resource.py | 130 +- tests/unit/test_configurator/conftest.py | 91 +- .../test_configurator/fixtures/logging.py | 17 +- .../test_configurator/fixtures/resource.py | 13 +- .../unit/test_configurator/fixtures/trace.py | 20 +- .../test_configurator_configure_otel.py | 6 +- .../test_configurator_metrics_init.py | 40 +- .../test_configurator_propagators.py | 106 +- .../test_configurator_span_processors.py | 25 +- .../test_configurator_traces_init.py | 20 +- tests/unit/test_distro.py | 855 +++++---- tests/unit/test_k8s.py | 63 +- tests/unit/test_oboe/test_dice.py | 4 +- tests/unit/test_oboe/test_http_sampler.py | 77 +- tests/unit/test_oboe/test_json_sampler.py | 139 +- tests/unit/test_oboe/test_oboe_sampler.py | 1639 +++++++++++------ tests/unit/test_oboe/test_sampler.py | 357 ++-- tests/unit/test_oboe/test_settings.py | 70 +- tests/unit/test_oboe/test_token_bucket.py | 19 +- tests/unit/test_oboe/test_trace_options.py | 400 +++- .../test_transaction_name_calculator.py | 106 +- .../test_oboe/test_transaction_name_pool.py | 14 +- tests/unit/test_processors/__init__.py | 2 +- .../test_response_time_processor.py | 1006 +++++----- .../test_serviceentry_processor.py | 98 +- tests/unit/test_propagator.py | 434 ++--- tests/unit/test_response_propagator.py | 88 +- tests/unit/test_sampler/__init__.py | 2 +- tests/unit/test_sampler/test_sampler.py | 22 +- tests/unit/test_semconv/__init__.py | 2 +- tests/unit/test_semconv/test_trace.py | 11 +- tests/unit/test_transaction_name_pool_api.py | 3 +- tests/unit/test_uams.py | 31 +- tests/unit/test_w3c_transformer.py | 47 +- tests/unit/test_xtraceoptions.py | 76 +- 63 files changed, 5919 insertions(+), 4113 deletions(-) create mode 100644 tests/unit/test_apm_config/conftest.py diff --git a/lambda/tests/test_instrumentation.py b/lambda/tests/test_instrumentation.py index 26b2a238b..01b0f5183 100644 --- a/lambda/tests/test_instrumentation.py +++ b/lambda/tests/test_instrumentation.py @@ -42,11 +42,8 @@ TraceContextTextMapPropagator, ) - AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER" -INIT_OTEL_SCRIPTS_DIR = os.path.join( - *(os.path.dirname(__file__), "..") -) +INIT_OTEL_SCRIPTS_DIR = os.path.join(*(os.path.dirname(__file__), "..")) TOX_PYTHON_DIRECTORY = os.path.dirname(os.path.dirname(which("python3"))) @@ -89,6 +86,7 @@ def __init__(self, function_name, aws_request_id, invoked_function_arn): MOCK_W3C_TRACE_STATE_KEY = "vendor_specific_key" MOCK_W3C_TRACE_STATE_VALUE = "test_value" + def replace_in_file(filename, old_text, new_text): with fileinput.FileInput(filename, inplace=True) as file_object: for line in file_object: @@ -122,7 +120,7 @@ def mock_aws_lambda_exec_wrapper(): ) # NOTE: Like opentelemetry-lambda, `solarwinds-apm/wrapper` cannot affect - # this python environment. We parse the stdout produced by our test python + # this python environment. We parse the stdout produced by our test python # program to update the environment in this parent python process. for env_var_line in completed_subprocess.stdout.split("\n"): @@ -289,4 +287,4 @@ def test_parent_context_from_lambda_event(self): ) self.assertTrue(parent_context.is_remote) - test_env_patch.stop() \ No newline at end of file + test_env_patch.stop() diff --git a/scripts/lint_and_format.py b/scripts/lint_and_format.py index 878f7d05a..282607c28 100644 --- a/scripts/lint_and_format.py +++ b/scripts/lint_and_format.py @@ -12,7 +12,9 @@ def parse_args(args=None): - parser = argparse.ArgumentParser(description="Lint and format everything, autofixing if possible.") + parser = argparse.ArgumentParser( + description="Lint and format everything, autofixing if possible." + ) parser.add_argument("--check-only", action="store_true") parser.add_argument("--allowexitcodes", action="append", default=[0]) parser.set_defaults(parser=parser) @@ -25,7 +27,7 @@ def run_subprocess(args, allowexitcodes): result = subprocess.run(args) if result is not None and result.returncode not in allowexitcodes: print( - "'{}' failed with code {}".format(args[0], result.returncode), + f"'{args[0]}' failed with code {result.returncode}", file=sys.stderr, ) sys.exit(result.returncode) diff --git a/tests/docker/install/app.py b/tests/docker/install/app.py index 024ebedb6..aaf58a690 100755 --- a/tests/docker/install/app.py +++ b/tests/docker/install/app.py @@ -5,13 +5,13 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. import requests - from flask import Flask from opentelemetry import trace app = Flask(__name__) tracer = trace.get_tracer(__name__) + @app.route("/test/") def test_trace(): """Makes request traced by autoinstrumentation @@ -21,6 +21,8 @@ def test_trace(): current_span.set_attribute("test.custom_attribute", "outer-foo-bar") with tracer.start_as_current_span("test_manual_inner"): current_span = trace.get_current_span() - current_span.set_attribute("test.custom_attribute", "inner-foo-bar") + current_span.set_attribute( + "test.custom_attribute", "inner-foo-bar" + ) requests.get("http://www.solarwinds.com/") return "Done" diff --git a/tests/docker/install/client.py b/tests/docker/install/client.py index 124f33de5..aea2026c0 100755 --- a/tests/docker/install/client.py +++ b/tests/docker/install/client.py @@ -8,15 +8,15 @@ import os import sys import time -import requests +import requests level = logging.DEBUG logger = logging.getLogger() logger.setLevel(level) handler = logging.StreamHandler(sys.stdout) handler.setLevel(level) -formatter = logging.Formatter('%(levelname)s | %(message)s') +formatter = logging.Formatter("%(levelname)s | %(message)s") handler.setFormatter(formatter) logger.addHandler(handler) @@ -24,22 +24,28 @@ def request_server(attempts=10): # Brute force until server responds try: - resp = requests.get("http://{}:{}/test/".format( - os.getenv("FLASK_RUN_HOST"), - os.getenv("FLASK_RUN_PORT"), - )) + resp = requests.get( + "http://{}:{}/test/".format( + os.getenv("FLASK_RUN_HOST"), + os.getenv("FLASK_RUN_PORT"), + ) + ) logger.debug("Response headers from Flask server:") logger.debug(resp.headers) except Exception: - logger.debug("Server not responding. Will try up to {} more times".format(attempts)) + logger.debug( + f"Server not responding. Will try up to {attempts} more times" + ) attempts -= 1 if attempts > 0: time.sleep(1) request_server(attempts) else: - sys.exit("ERROR: No response from instrumented test server after several attempts.") + sys.exit( + "ERROR: No response from instrumented test server after several attempts." + ) except (KeyboardInterrupt, SystemExit) as exc: - logger.debug("Exiting with: {e}".format(e=exc)) + logger.debug(f"Exiting with: {exc}") if __name__ == "__main__": diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 9167e547e..c3eb85d0c 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -2,4 +2,4 @@ # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/tests/integration/test_base_sw_headers_attrs.py b/tests/integration/test_base_sw_headers_attrs.py index 0dc73a9ad..57eb177ce 100644 --- a/tests/integration/test_base_sw_headers_attrs.py +++ b/tests/integration/test_base_sw_headers_attrs.py @@ -9,9 +9,6 @@ import flask import requests -from werkzeug.test import Client -from werkzeug.wrappers import Response - from opentelemetry import trace as trace_api from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor @@ -30,6 +27,8 @@ ) from opentelemetry.test.test_base import TestBase from opentelemetry.util._importlib_metadata import entry_points +from werkzeug.test import Client +from werkzeug.wrappers import Response from solarwinds_apm.apm_config import SolarWindsApmConfig from solarwinds_apm.configurator import SolarWindsConfigurator @@ -38,7 +37,6 @@ from solarwinds_apm.propagator import SolarWindsPropagator - class TestBaseSwHeadersAndAttributes(TestBase): """ Base class for testing SolarWinds custom distro header propagation @@ -49,7 +47,7 @@ class TestBaseSwHeadersAndAttributes(TestBase): "BucketCapacity", "BucketRate", "SampleRate", - "SampleSource" + "SampleSource", ] @staticmethod @@ -74,7 +72,7 @@ def _test_trace(): # WSGI capitalizes incoming HTTP headers incoming_headers.update({k.lower(): v.lower()}) - resp = requests.get(f"http://postman-echo.com/headers") + resp = requests.get("http://postman-echo.com/headers") # The return type must be a string, dict, tuple, Response instance, or WSGI callable # (not CaseInsensitiveDict) @@ -83,7 +81,7 @@ def _test_trace(): "tracestate": resp.request.headers["tracestate"], "incoming-headers": incoming_headers, } - + def _setup_endpoints(self): # pylint: disable=no-member self.app.route("/test_trace/")(self._test_trace) @@ -96,14 +94,14 @@ def setUp(self): # Load OTel env vars entry points argument_otel_environment_variable = {} for entry_point in iter( - entry_points( - group="opentelemetry_environment_variables" - ) + entry_points(group="opentelemetry_environment_variables") ): environment_variable_module = entry_point.load() for attribute in dir(environment_variable_module): if attribute.startswith("OTEL_"): - argument = re.sub(r"OTEL_(PYTHON_)?", "", attribute).lower() + argument = re.sub( + r"OTEL_(PYTHON_)?", "", attribute + ).lower() argument_otel_environment_variable[argument] = attribute # Set APM service key - not valid, but we mock anyway @@ -111,7 +109,9 @@ def setUp(self): # Load Distro SolarWindsDistro().configure() - assert os.environ["OTEL_PROPAGATORS"] == "solarwinds_propagator,baggage" + assert ( + os.environ["OTEL_PROPAGATORS"] == "solarwinds_propagator,baggage" + ) # Load Configurator to Configure SW custom SDK components # except use TestBase InMemorySpanExporter @@ -127,9 +127,13 @@ def setUp(self): reset_metrics_globals() # Init parent-based with JsonSampler to guarantee sampling decision for tests self.metric_reader = InMemoryMetricReader() - self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) + self.meter_provider = MeterProvider( + metric_readers=[self.metric_reader] + ) set_meter_provider(self.meter_provider) - sampler_configuration = SolarWindsApmConfig.to_configuration(apm_config) + sampler_configuration = SolarWindsApmConfig.to_configuration( + apm_config + ) json_sampler = JsonSampler( self.meter_provider, sampler_configuration, diff --git a/tests/integration/test_scenario_1.py b/tests/integration/test_scenario_1.py index 7844f2b7d..66fded108 100644 --- a/tests/integration/test_scenario_1.py +++ b/tests/integration/test_scenario_1.py @@ -4,8 +4,8 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -import re import json +import re import time from unittest import mock @@ -13,6 +13,7 @@ from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes + class TestScenario1(TestBaseSwHeadersAndAttributes): """ Test class for starting a new tracing decision with no input headers. @@ -40,32 +41,28 @@ def test_scenario_1_sampled(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): # Request to instrumented app, no traceparent/tracestate resp = self.client.get( - "/test_trace/", - headers={ - "some-header": "some-value" - } + "/test_trace/", headers={"some-header": "some-value"} ) resp_json = json.loads(resp.data) @@ -73,19 +70,19 @@ def test_scenario_1_sampled(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -98,7 +95,7 @@ def test_scenario_1_sampled(self): assert "tracestate" in resp_json # In this test we know there is only `sw` in tracestate # and its value will be new_span_id and new_trace_flags - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -119,7 +116,9 @@ def test_scenario_1_sampled(self): # Check root span tracestate has no `sw` key # because no valid parent context expected_trace_state = trace_api.TraceState([]) - assert span_server.context.trace_state.get("sw") == expected_trace_state.get("sw") # None + assert span_server.context.trace_state.get( + "sw" + ) == expected_trace_state.get("sw") # None # Check root span attributes # :present: @@ -127,30 +126,38 @@ def test_scenario_1_sampled(self): # :absent: # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # SWKeys, because no xtraceoptions in otel context - assert all(attr_key in span_server.attributes for attr_key in self.SW_SETTINGS_KEYS) + assert all( + attr_key in span_server.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) assert span_server.attributes["BucketCapacity"] == 2 assert span_server.attributes["BucketRate"] == 1 assert span_server.attributes["SampleRate"] == 1000000 assert span_server.attributes["SampleSource"] == 6 - assert not "sw.tracestate_parent_id" in span_server.attributes - assert not "SWKeys" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes + assert "SWKeys" not in span_server.attributes # Check outgoing request span tracestate has no `sw` key # because no valid parent context expected_trace_state = trace_api.TraceState([]) - assert span_client.context.trace_state.get("sw") == expected_trace_state.get("sw") # None + assert span_client.context.trace_state.get( + "sw" + ) == expected_trace_state.get("sw") # None # Check outgoing request span attributes # :absent: # service entry internal KVs, which are only on entry spans # sw.tracestate_parent_id, because cannot be set without attributes at decision # SWKeys, because no xtraceoptions in otel context - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id diff --git a/tests/integration/test_scenario_4.py b/tests/integration/test_scenario_4.py index 809773204..ba326940c 100644 --- a/tests/integration/test_scenario_4.py +++ b/tests/integration/test_scenario_4.py @@ -4,8 +4,8 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -import re import json +import re import time from unittest import mock @@ -34,9 +34,9 @@ def test_scenario_4_sampled(self): trace_id = "11112222333344445555666677778888" span_id = "1000100010001000" trace_flags = "01" - traceparent = "00-{}-{}-{}".format(trace_id, span_id, trace_flags) + traceparent = f"00-{trace_id}-{span_id}-{trace_flags}" tracestate_span = "e000baa4e000baa4" - tracestate = "sw={}-{}".format(tracestate_span, trace_flags) + tracestate = f"sw={tracestate_span}-{trace_flags}" # Use in-process test app client and mock to propagate context # and create in-memory trace @@ -47,23 +47,22 @@ def test_scenario_4_sampled(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): @@ -73,8 +72,8 @@ def test_scenario_4_sampled(self): headers={ "traceparent": traceparent, "tracestate": tracestate, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -82,19 +81,19 @@ def test_scenario_4_sampled(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, trace_flags from original request # - tracestate from original request assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -110,22 +109,30 @@ def test_scenario_4_sampled(self): assert "tracestate" in resp_json # In this test we know there is only `sw` in tracestate # and its value will be new_span_id and new_trace_flags - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify the OTel context extracted from the original request are continued by # the trace context injected into test app's outgoing postman-echo call try: assert resp_json["incoming-headers"]["traceparent"] == traceparent assert new_trace_id in resp_json["incoming-headers"]["traceparent"] - assert new_span_id not in resp_json["incoming-headers"]["traceparent"] - assert new_trace_flags in resp_json["incoming-headers"]["traceparent"] + assert ( + new_span_id not in resp_json["incoming-headers"]["traceparent"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["traceparent"] + ) assert resp_json["incoming-headers"]["tracestate"] == tracestate assert "sw=" in resp_json["incoming-headers"]["tracestate"] - assert new_span_id not in resp_json["incoming-headers"]["tracestate"] - assert new_trace_flags in resp_json["incoming-headers"]["tracestate"] + assert ( + new_span_id not in resp_json["incoming-headers"]["tracestate"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["tracestate"] + ) except KeyError as e: - self.fail("KeyError was raised at continue trace check: {}".format(e)) + self.fail(f"KeyError was raised at continue trace check: {e}") # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -145,8 +152,8 @@ def test_scenario_4_sampled(self): # Check spans' trace_id, which should match traceparent of original request # Note: context.trace_id needs a 32-byte hex conversion first. - assert "{:032x}".format(span_server.context.trace_id) == trace_id - assert "{:032x}".format(span_client.context.trace_id) == trace_id + assert f"{span_server.context.trace_id:032x}" == trace_id + assert f"{span_client.context.trace_id:032x}" == trace_id # Check service entry span attributes # :present: @@ -154,25 +161,34 @@ def test_scenario_4_sampled(self): # :absent: # service entry internal KVs, which not on entry spans if non-root # SWKeys, because no xtraceoptions in otel context - assert not any(attr_key in span_server.attributes for attr_key in self.SW_SETTINGS_KEYS) + assert not any( + attr_key in span_server.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) assert "sw.tracestate_parent_id" in span_server.attributes - assert span_server.attributes["sw.tracestate_parent_id"] == tracestate_span - assert not "SWKeys" in span_server.attributes + assert ( + span_server.attributes["sw.tracestate_parent_id"] + == tracestate_span + ) + assert "SWKeys" not in span_server.attributes # Check outgoing request span attributes # :absent: # service entry internal KVs, which are only on entry spans # sw.tracestate_parent_id, because cannot be set without attributes at decision # SWKeys, because no xtraceoptions in otel context - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id def test_scenario_4_not_sampled(self): """ @@ -186,9 +202,9 @@ def test_scenario_4_not_sampled(self): trace_id = "11112222333344445555666677778888" span_id = "1000100010001000" trace_flags = "00" - traceparent = "00-{}-{}-{}".format(trace_id, span_id, trace_flags) + traceparent = f"00-{trace_id}-{span_id}-{trace_flags}" tracestate_span = "e000baa4e000baa4" - tracestate = "sw={}-{}".format(tracestate_span, trace_flags) + tracestate = f"sw={tracestate_span}-{trace_flags}" # Use in-process test app client and mock to propagate context # and create in-memory trace @@ -199,23 +215,22 @@ def test_scenario_4_not_sampled(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -225,8 +240,8 @@ def test_scenario_4_not_sampled(self): headers={ "traceparent": traceparent, "tracestate": tracestate, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -234,19 +249,19 @@ def test_scenario_4_not_sampled(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, trace_flags from original request # - tracestate from original request assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -262,22 +277,30 @@ def test_scenario_4_not_sampled(self): assert "tracestate" in resp_json # In this test we know there is only `sw` in tracestate # and its value will be new_span_id and new_trace_flags - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify the OTel context extracted from the original request are continued by # the trace context injected into test app's outgoing postman-echo call try: assert resp_json["incoming-headers"]["traceparent"] == traceparent assert new_trace_id in resp_json["incoming-headers"]["traceparent"] - assert new_span_id not in resp_json["incoming-headers"]["traceparent"] - assert new_trace_flags in resp_json["incoming-headers"]["traceparent"] + assert ( + new_span_id not in resp_json["incoming-headers"]["traceparent"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["traceparent"] + ) assert resp_json["incoming-headers"]["tracestate"] == tracestate assert "sw=" in resp_json["incoming-headers"]["tracestate"] - assert new_span_id not in resp_json["incoming-headers"]["tracestate"] - assert new_trace_flags in resp_json["incoming-headers"]["tracestate"] + assert ( + new_span_id not in resp_json["incoming-headers"]["tracestate"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["tracestate"] + ) except KeyError as e: - self.fail("KeyError was raised at continue trace check: {}".format(e)) + self.fail(f"KeyError was raised at continue trace check: {e}") # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask diff --git a/tests/integration/test_scenario_8.py b/tests/integration/test_scenario_8.py index 4b5d5b364..2cf883844 100644 --- a/tests/integration/test_scenario_8.py +++ b/tests/integration/test_scenario_8.py @@ -4,8 +4,8 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -import re import json +import re import time from unittest import mock @@ -38,10 +38,10 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): trace_id = "11112222333344445555666677778888" span_id = "1000100010001000" trace_flags = "01" - traceparent = "00-{}-{}-{}".format(trace_id, span_id, trace_flags) + traceparent = f"00-{trace_id}-{span_id}-{trace_flags}" tracestate_span = "e000baa4e000baa4" - tracestate = "sw={}-{}".format(tracestate_span, trace_flags) - xtraceoptions = "trigger-trace;custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={}".format(1234567890) + tracestate = f"sw={tracestate_span}-{trace_flags}" + xtraceoptions = f"trigger-trace;custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={1234567890}" # Use in-process test app client and mock to propagate context # and create in-memory trace @@ -52,23 +52,22 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000, + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): @@ -79,8 +78,8 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): "traceparent": traceparent, "tracestate": tracestate, "x-trace-options": xtraceoptions, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -88,7 +87,7 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: @@ -96,12 +95,12 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): # - tracestate from original request assert "traceparent" in resp_json assert trace_id in resp_json["traceparent"] - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -118,25 +117,30 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify the OTel context extracted from the original request are continued by # the trace context injected into test app's outgoing postman-echo call try: assert resp_json["incoming-headers"]["traceparent"] == traceparent assert new_trace_id in resp_json["incoming-headers"]["traceparent"] - assert new_span_id not in resp_json["incoming-headers"]["traceparent"] - assert new_trace_flags in resp_json["incoming-headers"]["traceparent"] + assert ( + new_span_id not in resp_json["incoming-headers"]["traceparent"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["traceparent"] + ) assert resp_json["incoming-headers"]["tracestate"] == tracestate assert "sw=" in resp_json["incoming-headers"]["tracestate"] - assert new_span_id not in resp_json["incoming-headers"]["tracestate"] - assert new_trace_flags in resp_json["incoming-headers"]["tracestate"] + assert ( + new_span_id not in resp_json["incoming-headers"]["tracestate"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["tracestate"] + ) except KeyError as e: - self.fail("KeyError was raised at continue trace check: {}".format(e)) + self.fail(f"KeyError was raised at continue trace check: {e}") # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -147,7 +151,9 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): # Verify x-trace-options-response response header present # with values calculated from decision and input validation assert "x-trace-options-response" in resp.headers - assert "trigger-trace=ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=ignored" in resp.headers["x-trace-options-response"] + ) assert "ignored=foo" in resp.headers["x-trace-options-response"] # Verify spans exported: service entry + outgoing request (child with local parent) @@ -162,19 +168,26 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): # Check spans' trace_id, which should match traceparent of original request # Note: context.trace_id needs a 32-byte hex conversion first. - assert "{:032x}".format(span_server.context.trace_id) == trace_id - assert "{:032x}".format(span_client.context.trace_id) == trace_id + assert f"{span_server.context.trace_id:032x}" == trace_id + assert f"{span_client.context.trace_id:032x}" == trace_id # Check service entry span tracestate has `sw` and `xtrace_options_response` key. # In this test it should be span_id, traceflags from extracted traceparent. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("sw", "{}-{}".format(tracestate_span, trace_flags)), - ("xtrace_options_response", "trigger-trace####ignored;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ("sw", f"{tracestate_span}-{trace_flags}"), + ( + "xtrace_options_response", + "trigger-trace####ignored;ignored####foo", + ), + ] + ) actual_trace_state = span_server.context.trace_state - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # Check service entry span attributes @@ -184,24 +197,39 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): # SWKeys, because included in xtraceoptions in otel context # :absent: # service entry internal KVs, which not on entry spans if non-root - assert not any(attr_key in span_server.attributes for attr_key in self.SW_SETTINGS_KEYS) + assert not any( + attr_key in span_server.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) assert "sw.tracestate_parent_id" in span_server.attributes - assert span_server.attributes["sw.tracestate_parent_id"] == tracestate_span + assert ( + span_server.attributes["sw.tracestate_parent_id"] + == tracestate_span + ) assert "custom-from" in span_server.attributes assert span_server.attributes["custom-from"] == "lin" assert "SWKeys" in span_server.attributes - assert span_server.attributes["SWKeys"] == "custom-sw-from:tammy,baz:qux" + assert ( + span_server.attributes["SWKeys"] == "custom-sw-from:tammy,baz:qux" + ) # Check service entry span tracestate has `sw` and `xtrace_options_response` key. # In this test it should also be span_id, traceflags from extracted traceparent # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("sw", "{}-{}".format(tracestate_span, trace_flags)), - ("xtrace_options_response", "trigger-trace####ignored;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ("sw", f"{tracestate_span}-{trace_flags}"), + ( + "xtrace_options_response", + "trigger-trace####ignored;ignored####foo", + ), + ] + ) actual_trace_state = span_client.context.trace_state - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # Check outgoing request span attributes @@ -210,16 +238,19 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid(self): # sw.tracestate_parent_id, because cannot be set without attributes at decision # custom-*, because only on entry spans # SWKeys, because only on entry spans - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "custom-from" in span_client.attributes - assert not "SWKeys" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "custom-from" not in span_client.attributes + assert "SWKeys" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): """ @@ -236,10 +267,10 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): trace_id = "11112222333344445555666677778888" span_id = "1000100010001000" trace_flags = "00" - traceparent = "00-{}-{}-01".format(trace_id, span_id) + traceparent = f"00-{trace_id}-{span_id}-01" tracestate_span = "e000baa4e000baa4" - tracestate = "sw={}-{}".format(tracestate_span, trace_flags) - xtraceoptions = "trigger-trace;custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={}".format(1234567890) + tracestate = f"sw={tracestate_span}-{trace_flags}" + xtraceoptions = f"trigger-trace;custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={1234567890}" # Use in-process test app client and mock to propagate context # and create in-memory trace @@ -250,23 +281,22 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0, + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -277,8 +307,8 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): "traceparent": traceparent, "tracestate": tracestate, "x-trace-options": xtraceoptions, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -286,7 +316,7 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: @@ -294,12 +324,12 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): # - tracestate from original request assert "traceparent" in resp_json assert trace_id in resp_json["traceparent"] - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -316,25 +346,30 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify the OTel context extracted from the original request are continued by # the trace context injected into test app's outgoing postman-echo call try: assert resp_json["incoming-headers"]["traceparent"] == traceparent assert new_trace_id in resp_json["incoming-headers"]["traceparent"] - assert new_span_id not in resp_json["incoming-headers"]["traceparent"] - assert new_trace_flags in resp_json["incoming-headers"]["traceparent"] + assert ( + new_span_id not in resp_json["incoming-headers"]["traceparent"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["traceparent"] + ) assert resp_json["incoming-headers"]["tracestate"] == tracestate assert "sw=" in resp_json["incoming-headers"]["tracestate"] - assert new_span_id not in resp_json["incoming-headers"]["tracestate"] - assert new_trace_flags in resp_json["incoming-headers"]["tracestate"] + assert ( + new_span_id not in resp_json["incoming-headers"]["tracestate"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["tracestate"] + ) except KeyError as e: - self.fail("KeyError was raised at continue trace check: {}".format(e)) + self.fail(f"KeyError was raised at continue trace check: {e}") # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -345,14 +380,18 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid(self): # Verify x-trace-options-response response header present # with values calculated from decision and input validation assert "x-trace-options-response" in resp.headers - assert "trigger-trace=ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=ignored" in resp.headers["x-trace-options-response"] + ) assert "ignored=foo" in resp.headers["x-trace-options-response"] # Verify no spans exported spans = self.memory_exporter.get_finished_spans() assert len(spans) == 0 - def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): + def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt( + self, + ): """ 1. Decision to sample is continued using valid extracted tracestate at service entry span (mocked). Unsigned trigger trace header is ignored. This is @@ -370,10 +409,10 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): trace_id = "11112222333344445555666677778888" span_id = "1000100010001000" trace_flags = "01" - traceparent = "00-{}-{}-{}".format(trace_id, span_id, trace_flags) + traceparent = f"00-{trace_id}-{span_id}-{trace_flags}" tracestate_span = "e000baa4e000baa4" - tracestate = "sw={}-{}".format(tracestate_span, trace_flags) - xtraceoptions = "custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={}".format(1234567890) + tracestate = f"sw={tracestate_span}-{trace_flags}" + xtraceoptions = f"custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={1234567890}" # Use in-process test app client and mock to propagate context # and create in-memory trace @@ -384,23 +423,22 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000, + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): @@ -411,8 +449,8 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): "traceparent": traceparent, "tracestate": tracestate, "x-trace-options": xtraceoptions, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -420,7 +458,7 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: @@ -428,12 +466,12 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): # - tracestate from original request assert "traceparent" in resp_json assert trace_id in resp_json["traceparent"] - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -451,25 +489,30 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): # where value will be new_span_id and new_trace_flags. # There should be no `xtrace_options_response` key because there is # no trigger-trace in the extracted x-trace-options header. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify the OTel context extracted from the original request are continued by # the trace context injected into test app's outgoing postman-echo call try: assert resp_json["incoming-headers"]["traceparent"] == traceparent assert new_trace_id in resp_json["incoming-headers"]["traceparent"] - assert new_span_id not in resp_json["incoming-headers"]["traceparent"] - assert new_trace_flags in resp_json["incoming-headers"]["traceparent"] + assert ( + new_span_id not in resp_json["incoming-headers"]["traceparent"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["traceparent"] + ) assert resp_json["incoming-headers"]["tracestate"] == tracestate assert "sw=" in resp_json["incoming-headers"]["tracestate"] - assert new_span_id not in resp_json["incoming-headers"]["tracestate"] - assert new_trace_flags in resp_json["incoming-headers"]["tracestate"] + assert ( + new_span_id not in resp_json["incoming-headers"]["tracestate"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["tracestate"] + ) except KeyError as e: - self.fail("KeyError was raised at continue trace check: {}".format(e)) + self.fail(f"KeyError was raised at continue trace check: {e}") # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -480,7 +523,10 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): # Verify x-trace-options-response response header present # with values calculated from decision and input validation assert "x-trace-options-response" in resp.headers - assert "trigger-trace=not-requested" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=not-requested" + in resp.headers["x-trace-options-response"] + ) assert "ignored=foo" in resp.headers["x-trace-options-response"] # Verify spans exported: service entry + outgoing request (child with local parent) @@ -495,20 +541,27 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): # Check spans' trace_id, which should match traceparent of original request # Note: context.trace_id needs a 32-byte hex conversion first. - assert "{:032x}".format(span_server.context.trace_id) == trace_id - assert "{:032x}".format(span_client.context.trace_id) == trace_id + assert f"{span_server.context.trace_id:032x}" == trace_id + assert f"{span_client.context.trace_id:032x}" == trace_id # Check service entry span tracestate has `sw` and `xtrace_options_response` keys # In this test it should be span_id, traceflags from extracted traceparent. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("sw", "{}-{}".format(tracestate_span, trace_flags)), - ("xtrace_options_response", "trigger-trace####not-requested;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ("sw", f"{tracestate_span}-{trace_flags}"), + ( + "xtrace_options_response", + "trigger-trace####not-requested;ignored####foo", + ), + ] + ) actual_trace_state = span_server.context.trace_state assert actual_trace_state.get("sw") == expected_trace_state.get("sw") - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check service entry span attributes # :present: @@ -517,25 +570,40 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): # SWKeys, because included in xtraceoptions in otel context # :absent: # service entry internal KVs, which not on entry spans if non-root - assert not any(attr_key in span_server.attributes for attr_key in self.SW_SETTINGS_KEYS) + assert not any( + attr_key in span_server.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) assert "sw.tracestate_parent_id" in span_server.attributes - assert span_server.attributes["sw.tracestate_parent_id"] == tracestate_span + assert ( + span_server.attributes["sw.tracestate_parent_id"] + == tracestate_span + ) assert "custom-from" in span_server.attributes assert span_server.attributes["custom-from"] == "lin" assert "SWKeys" in span_server.attributes - assert span_server.attributes["SWKeys"] == "custom-sw-from:tammy,baz:qux" + assert ( + span_server.attributes["SWKeys"] == "custom-sw-from:tammy,baz:qux" + ) # Check outgoing request tracestate has `sw` and `xtrace_options_response` keys # In this test it should also be span_id, traceflags from extracted traceparent # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("sw", "{}-{}".format(tracestate_span, trace_flags)), - ("xtrace_options_response", "trigger-trace####not-requested;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ("sw", f"{tracestate_span}-{trace_flags}"), + ( + "xtrace_options_response", + "trigger-trace####not-requested;ignored####foo", + ), + ] + ) actual_trace_state = span_client.context.trace_state assert actual_trace_state.get("sw") == expected_trace_state.get("sw") - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check outgoing request span attributes # :absent: @@ -543,18 +611,23 @@ def test_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): # sw.tracestate_parent_id, because cannot be set without attributes at decision # custom-*, because only on entry spans # SWKeys, because only on entry spans - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "custom-from" in span_client.attributes - assert not "SWKeys" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "custom-from" not in span_client.attributes + assert "SWKeys" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id - def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self): + def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt( + self, + ): """ 1. Decision to NOT sample is continued using valid extracted tracestate at service entry span (mocked). Unsigned trigger trace header is ignored. This is @@ -568,10 +641,10 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self) trace_id = "11112222333344445555666677778888" span_id = "1000100010001000" trace_flags = "00" - traceparent = "00-{}-{}-01".format(trace_id, span_id) + traceparent = f"00-{trace_id}-{span_id}-01" tracestate_span = "e000baa4e000baa4" - tracestate = "sw={}-{}".format(tracestate_span, trace_flags) - xtraceoptions = "custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={}".format(1234567890) + tracestate = f"sw={tracestate_span}-{trace_flags}" + xtraceoptions = f"custom-from=lin;foo=bar;sw-keys=custom-sw-from:tammy,baz:qux;ts={1234567890}" # Use in-process test app client and mock to propagate context # and create in-memory trace @@ -582,23 +655,22 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self) target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0, + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -609,8 +681,8 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self) "traceparent": traceparent, "tracestate": tracestate, "x-trace-options": xtraceoptions, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -618,7 +690,7 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self) try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: @@ -626,12 +698,12 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self) # - tracestate from original request assert "traceparent" in resp_json assert trace_id in resp_json["traceparent"] - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -649,25 +721,30 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self) # where value will be new_span_id and new_trace_flags. # There should be no `xtrace_options_response` key because there is # no trigger-trace in the extracted x-trace-options header. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify the OTel context extracted from the original request are continued by # the trace context injected into test app's outgoing postman-echo call try: assert resp_json["incoming-headers"]["traceparent"] == traceparent assert new_trace_id in resp_json["incoming-headers"]["traceparent"] - assert new_span_id not in resp_json["incoming-headers"]["traceparent"] - assert new_trace_flags in resp_json["incoming-headers"]["traceparent"] + assert ( + new_span_id not in resp_json["incoming-headers"]["traceparent"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["traceparent"] + ) assert resp_json["incoming-headers"]["tracestate"] == tracestate assert "sw=" in resp_json["incoming-headers"]["tracestate"] - assert new_span_id not in resp_json["incoming-headers"]["tracestate"] - assert new_trace_flags in resp_json["incoming-headers"]["tracestate"] + assert ( + new_span_id not in resp_json["incoming-headers"]["tracestate"] + ) + assert ( + new_trace_flags in resp_json["incoming-headers"]["tracestate"] + ) except KeyError as e: - self.fail("KeyError was raised at continue trace check: {}".format(e)) + self.fail(f"KeyError was raised at continue trace check: {e}") # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -678,7 +755,10 @@ def test_not_sampled_both_trace_context_and_xtraceoptions_valid_without_tt(self) # Verify x-trace-options-response response header present # with values calculated from decision and input validation assert "x-trace-options-response" in resp.headers - assert "trigger-trace=not-requested" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=not-requested" + in resp.headers["x-trace-options-response"] + ) assert "ignored=foo" in resp.headers["x-trace-options-response"] # Verify no spans exported @@ -708,23 +788,22 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000, + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): @@ -735,8 +814,8 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): "traceparent": "not-a-valid-traceparent", "tracestate": "also-not-a-valid-tracestate", "x-trace-options": "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -744,19 +823,19 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -770,10 +849,7 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -785,7 +861,10 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): # with values calculated from decision and input validation assert "x-trace-options-response" in resp.headers assert "trigger-trace=ok" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify spans exported: service entry (root) + outgoing request (child with local parent) spans = self.memory_exporter.get_finished_spans() @@ -801,12 +880,21 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_server.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check root span attributes # :present: @@ -818,29 +906,47 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): # service entry internal KVs for Sample* # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # the ignored value in the x-trace-options-header - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) - assert not any(attr_key in span_server.attributes for attr_key in ["SampleRate", "SampleSource"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) + assert not any( + attr_key in span_server.attributes + for attr_key in ["SampleRate", "SampleSource"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes - assert span_server.attributes["SWKeys"] == "check-id:check-1013,website-id:booking-demo" + assert ( + span_server.attributes["SWKeys"] + == "check-id:check-1013,website-id:booking-demo" + ) assert "custom-awesome-key" in span_server.attributes assert span_server.attributes["custom-awesome-key"] == "foo" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "this-will-be-ignored" not in span_server.attributes # Check root span tracestate has `xtrace_options_response` key but no `sw` key # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_client.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check outgoing request span attributes # :absent: @@ -850,20 +956,25 @@ def test_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-awesome-key" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-awesome-key" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "this-will-be-ignored" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id - def test_not_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): + def test_not_sampled_invalid_trace_context_and_valid_unsigned_with_tt( + self, + ): """ Scenario #6, not sampled with unsigned tt: 1. Decision to NOT sample with unsigned trigger trace flag is made at root/service @@ -883,23 +994,22 @@ def test_not_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":0, - "BucketRate":0, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":0, - "TriggerRelaxedBucketRate":0, - "TriggerStrictBucketCapacity":0, - "TriggerStrictBucketRate":0 - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0, + "arguments": { + "BucketCapacity": 0, + "BucketRate": 0, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 0, + "TriggerRelaxedBucketRate": 0, + "TriggerStrictBucketCapacity": 0, + "TriggerStrictBucketRate": 0, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -910,8 +1020,8 @@ def test_not_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): "traceparent": "not-a-valid-traceparent", "tracestate": "also-not-a-valid-tracestate", "x-trace-options": "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -919,19 +1029,19 @@ def test_not_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -945,7 +1055,7 @@ def test_not_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -956,8 +1066,14 @@ def test_not_sampled_invalid_trace_context_and_valid_unsigned_with_tt(self): # Verify x-trace-options-response response header present # with values calculated from decision and input validation assert "x-trace-options-response" in resp.headers - assert "trigger-trace=rate-exceeded" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=rate-exceeded" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify no spans exported spans = self.memory_exporter.get_finished_spans() diff --git a/tests/integration/test_service_instance_id.py b/tests/integration/test_service_instance_id.py index 570486460..ddd5de858 100644 --- a/tests/integration/test_service_instance_id.py +++ b/tests/integration/test_service_instance_id.py @@ -12,11 +12,15 @@ from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes -class TestServiceInstanceIdPrecedence1ResourceAttributes(TestBaseSwHeadersAndAttributes): +class TestServiceInstanceIdPrecedence1ResourceAttributes( + TestBaseSwHeadersAndAttributes +): """Test that OTEL_RESOURCE_ATTRIBUTES service.instance.id has highest priority.""" def setUp(self): - os.environ["OTEL_RESOURCE_ATTRIBUTES"] = "service.instance.id=resource-attr-instance-123" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( + "service.instance.id=resource-attr-instance-123" + ) # Set a full Azure App Service environment so the azure_app_service detector is active. os.environ["WEBSITE_SITE_NAME"] = "my-azure-app" os.environ["WEBSITE_RESOURCE_GROUP"] = "prod-rg" @@ -63,9 +67,15 @@ def test_resource_attributes_over_azure_detector(self): ): resource = self.configurator.apm_config.resource assert resource.attributes.get("cloud.provider") == "azure" - assert resource.attributes["service.instance.id"] == "resource-attr-instance-123" + assert ( + resource.attributes["service.instance.id"] + == "resource-attr-instance-123" + ) -class TestServiceInstanceIdPrecedence2AzureAppService(TestBaseSwHeadersAndAttributes): + +class TestServiceInstanceIdPrecedence2AzureAppService( + TestBaseSwHeadersAndAttributes +): """Test that Azure App Service WEBSITE_INSTANCE_ID overrides UUID fallback.""" def setUp(self): @@ -106,10 +116,15 @@ def test_azure_app_service_instance_id_over_uuid(self): ], ): resource = self.configurator.apm_config.resource - assert resource.attributes["service.instance.id"] == "azure-app-instance-abc123" + assert ( + resource.attributes["service.instance.id"] + == "azure-app-instance-abc123" + ) -class TestServiceInstanceIdPrecedence4UUIDFallback(TestBaseSwHeadersAndAttributes): +class TestServiceInstanceIdPrecedence4UUIDFallback( + TestBaseSwHeadersAndAttributes +): """Test that non-platform environments get UUID from ServiceInstanceIdResourceDetector.""" def setUp(self): @@ -152,7 +167,9 @@ def test_uuid_fallback_in_non_platform_environment(self): is_valid_uuid = True except (ValueError, TypeError, AttributeError): is_valid_uuid = False - assert is_valid_uuid, f"service.instance.id '{instance_id}' is not a valid UUID" + assert is_valid_uuid, ( + f"service.instance.id '{instance_id}' is not a valid UUID" + ) class TestServiceInstanceIdWithCustomDetectors(TestBaseSwHeadersAndAttributes): @@ -200,4 +217,6 @@ def test_sdk_auto_appends_service_instance_detector(self): is_valid_uuid = True except (ValueError, TypeError, AttributeError): is_valid_uuid = False - assert is_valid_uuid, f"service.instance.id '{instance_id}' is not a valid UUID" + assert is_valid_uuid, ( + f"service.instance.id '{instance_id}' is not a valid UUID" + ) diff --git a/tests/integration/test_service_name.py b/tests/integration/test_service_name.py index 4031c7645..f1983b950 100644 --- a/tests/integration/test_service_name.py +++ b/tests/integration/test_service_name.py @@ -11,8 +11,9 @@ from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes -class TestServiceNamePrecedence1OtelServiceName(TestBaseSwHeadersAndAttributes): - +class TestServiceNamePrecedence1OtelServiceName( + TestBaseSwHeadersAndAttributes +): def setUp(self): os.environ["OTEL_SERVICE_NAME"] = "otel-override" os.environ["WEBSITE_SITE_NAME"] = "azure-app" @@ -64,12 +65,13 @@ def test_otel_service_name_over_azure(self): assert resource_attrs["cloud.platform"] == "azure_app_service" -class TestServiceNamePrecedence2ResourceAttributes(TestBaseSwHeadersAndAttributes): - +class TestServiceNamePrecedence2ResourceAttributes( + TestBaseSwHeadersAndAttributes +): def setUp(self): - os.environ[ - "OTEL_RESOURCE_ATTRIBUTES" - ] = "service.name=resource-attr-name,deployment.environment=prod" + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( + "service.name=resource-attr-name,deployment.environment=prod" + ) os.environ["WEBSITE_SITE_NAME"] = "azure-app" super().setUp() @@ -115,7 +117,6 @@ def test_resource_attributes_over_detector(self): class TestServiceNamePrecedence3AzureDetector(TestBaseSwHeadersAndAttributes): - def setUp(self): os.environ["WEBSITE_SITE_NAME"] = "azure-production-app" os.environ["WEBSITE_RESOURCE_GROUP"] = "prod-rg" @@ -167,7 +168,6 @@ def test_azure_detector_over_sw_key(self): class TestServiceNamePrecedence4SwKeyFallback(TestBaseSwHeadersAndAttributes): - def setUp(self): # Base class will set SW_APM_SERVICE_KEY="foo:bar" super().setUp() @@ -212,8 +212,9 @@ def test_sw_key_fallback(self): assert isinstance(resource_attrs["host.name"], str) -class TestServiceNameAzureDetectorFullEnvironment(TestBaseSwHeadersAndAttributes): - +class TestServiceNameAzureDetectorFullEnvironment( + TestBaseSwHeadersAndAttributes +): def setUp(self): os.environ["WEBSITE_SITE_NAME"] = "my-azure-app" os.environ["WEBSITE_RESOURCE_GROUP"] = "production-rg" diff --git a/tests/integration/test_signed_tt.py b/tests/integration/test_signed_tt.py index 546bcc6c7..c29e59e9e 100644 --- a/tests/integration/test_signed_tt.py +++ b/tests/integration/test_signed_tt.py @@ -5,15 +5,15 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. import hashlib import hmac -import re import json +import re import time from unittest import mock from opentelemetry import trace as trace_api -from unittest import mock from solarwinds_apm.oboe.settings import LocalSettings, TracingMode + from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes @@ -47,36 +47,37 @@ def test_signed_with_tt_auth_ok(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"key", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "key", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): x_trace_options = f"trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp}" - x_trace_options_signature = hmac.new(str.encode("key"), str.encode(x_trace_options), hashlib.sha1).hexdigest() + x_trace_options_signature = hmac.new( + str.encode("key"), str.encode(x_trace_options), hashlib.sha1 + ).hexdigest() # Request to instrumented app with headers resp = self.client.get( "/test_trace/", headers={ "x-trace-options": x_trace_options, "x-trace-options-signature": x_trace_options_signature, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -84,19 +85,19 @@ def test_signed_with_tt_auth_ok(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -110,7 +111,7 @@ def test_signed_with_tt_auth_ok(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -121,7 +122,10 @@ def test_signed_with_tt_auth_ok(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers assert "trigger-trace=ok" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify spans exported: service entry (root) + outgoing request (child with local parent) spans = self.memory_exporter.get_finished_spans() @@ -137,12 +141,21 @@ def test_signed_with_tt_auth_ok(self): # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "auth####ok;trigger-trace####ok;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "auth####ok;trigger-trace####ok;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_server.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check root span attributes # :present: @@ -155,28 +168,43 @@ def test_signed_with_tt_auth_ok(self): # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity","BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 4 assert span_server.attributes["BucketRate"] == 3 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes - assert span_server.attributes["SWKeys"] == "check-id:check-1013,website-id:booking-demo" + assert ( + span_server.attributes["SWKeys"] + == "check-id:check-1013,website-id:booking-demo" + ) assert "custom-awesome-key" in span_server.attributes assert span_server.attributes["custom-awesome-key"] == "foo" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "this-will-be-ignored" not in span_server.attributes # Check client span tracestate has `xtrace_options_response` key but no `sw` key # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "auth####ok;trigger-trace####ok;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "auth####ok;trigger-trace####ok;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_client.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check outgoing request span attributes # :absent: @@ -186,18 +214,21 @@ def test_signed_with_tt_auth_ok(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-awesome-key" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-awesome-key" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "this-will-be-ignored" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id def test_signed_without_tt_auth_ok(self): """ @@ -223,37 +254,37 @@ def test_signed_without_tt_auth_ok(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"key", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5 - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1_000_000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "key", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1_000_000, } ], ): x_trace_options = f"sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp}" - x_trace_options_signature = hmac.new(str.encode("key"), str.encode(x_trace_options), - hashlib.sha1).hexdigest() + x_trace_options_signature = hmac.new( + str.encode("key"), str.encode(x_trace_options), hashlib.sha1 + ).hexdigest() # Request to instrumented app with headers resp = self.client.get( "/test_trace/", headers={ "x-trace-options": x_trace_options, "x-trace-options-signature": x_trace_options_signature, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -261,19 +292,19 @@ def test_signed_without_tt_auth_ok(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -287,10 +318,7 @@ def test_signed_without_tt_auth_ok(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -300,8 +328,14 @@ def test_signed_without_tt_auth_ok(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers - assert "trigger-trace=not-requested" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=not-requested" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify spans exported: service entry (root) + outgoing request (child with local parent) spans = self.memory_exporter.get_finished_spans() @@ -317,12 +351,21 @@ def test_signed_without_tt_auth_ok(self): # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "auth####ok;trigger-trace####not-requested;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "auth####ok;trigger-trace####not-requested;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_server.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check root span attributes # :present: @@ -335,12 +378,18 @@ def test_signed_without_tt_auth_ok(self): # TriggeredTrace, because trigger-trace not in otel context # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 2 assert span_server.attributes["BucketRate"] == 1 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes - assert span_server.attributes["SWKeys"] == "check-id:check-1013,website-id:booking-demo" + assert ( + span_server.attributes["SWKeys"] + == "check-id:check-1013,website-id:booking-demo" + ) assert "custom-awesome-key" in span_server.attributes assert span_server.attributes["custom-awesome-key"] == "foo" assert "TriggeredTrace" not in span_server.attributes @@ -350,12 +399,21 @@ def test_signed_without_tt_auth_ok(self): # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "auth####ok;trigger-trace####not-requested;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "auth####ok;trigger-trace####not-requested;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_client.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check outgoing request span attributes # :absent: @@ -365,18 +423,21 @@ def test_signed_without_tt_auth_ok(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-awesome-key" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-awesome-key" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "this-will-be-ignored" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id def test_signed_with_tt_rate_exceeded(self): """ @@ -396,37 +457,37 @@ def test_signed_with_tt_rate_exceeded(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"key", - "TriggerRelaxedBucketCapacity":0, - "TriggerRelaxedBucketRate":0, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "key", + "TriggerRelaxedBucketCapacity": 0, + "TriggerRelaxedBucketRate": 0, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): x_trace_options = f"trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp}" - x_trace_options_signature = hmac.new(str.encode("key"), str.encode(x_trace_options), - hashlib.sha1).hexdigest() + x_trace_options_signature = hmac.new( + str.encode("key"), str.encode(x_trace_options), hashlib.sha1 + ).hexdigest() # Request to instrumented app with headers resp = self.client.get( "/test_trace/", headers={ "x-trace-options": x_trace_options, "x-trace-options-signature": x_trace_options_signature, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -434,19 +495,19 @@ def test_signed_with_tt_rate_exceeded(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -460,7 +521,7 @@ def test_signed_with_tt_rate_exceeded(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -471,8 +532,14 @@ def test_signed_with_tt_rate_exceeded(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers assert "auth=ok" in resp.headers["x-trace-options-response"] - assert "trigger-trace=rate-exceeded" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=rate-exceeded" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify no spans exported spans = self.memory_exporter.get_finished_spans() @@ -493,65 +560,71 @@ def test_signed_with_tt_tracing_disabled(self): # Mock JSON read to guarantee sample decision timestamp = int(time.time()) - with mock.patch( - target="solarwinds_apm.oboe.sampler.Sampler.local_settings", - return_value=LocalSettings(tracing_mode=TracingMode.NEVER, trigger_mode=False) - ): - with mock.patch( + with ( + mock.patch( + target="solarwinds_apm.oboe.sampler.Sampler.local_settings", + return_value=LocalSettings( + tracing_mode=TracingMode.NEVER, trigger_mode=False + ), + ), + mock.patch( target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"key", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5 - }, - "flags":"", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1_000_000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "key", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1_000_000, } ], - ): - x_trace_options = f"trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp}" - x_trace_options_signature = hmac.new(str.encode("key"), str.encode(x_trace_options), - hashlib.sha1).hexdigest() - # Request to instrumented app with headers - resp = self.client.get( - "/test_trace/", - headers={ - "x-trace-options": x_trace_options, - "x-trace-options-signature": x_trace_options_signature, - "some-header": "some-value" - } - ) + ), + ): + x_trace_options = f"trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp}" + x_trace_options_signature = hmac.new( + str.encode("key"), + str.encode(x_trace_options), + hashlib.sha1, + ).hexdigest() + # Request to instrumented app with headers + resp = self.client.get( + "/test_trace/", + headers={ + "x-trace-options": x_trace_options, + "x-trace-options-signature": x_trace_options_signature, + "some-header": "some-value", + }, + ) resp_json = json.loads(resp.data) # Verify some-header was not altered by instrumentation try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -565,7 +638,7 @@ def test_signed_with_tt_tracing_disabled(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -576,8 +649,14 @@ def test_signed_with_tt_tracing_disabled(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers assert "auth=ok" in resp.headers["x-trace-options-response"] - assert "trigger-trace=tracing-disabled" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=tracing-disabled" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify no spans exported spans = self.memory_exporter.get_finished_spans() @@ -604,23 +683,22 @@ def test_signed_with_tt_auth_fail(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -630,8 +708,8 @@ def test_signed_with_tt_auth_fail(self): headers={ "x-trace-options": f"trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp}", "x-trace-options-signature": "bad-sig", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -639,19 +717,19 @@ def test_signed_with_tt_auth_fail(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -665,7 +743,7 @@ def test_signed_with_tt_auth_fail(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -676,7 +754,9 @@ def test_signed_with_tt_auth_fail(self): # Verify x-trace-options-response response header present # but only with 'auth' KV assert "x-trace-options-response" in resp.headers - assert "auth=no-signature-key" in resp.headers["x-trace-options-response"] + assert ( + "auth=no-signature-key" in resp.headers["x-trace-options-response"] + ) assert "ignored" not in resp.headers["x-trace-options-response"] # Verify no spans exported @@ -704,23 +784,22 @@ def test_signed_without_tt_auth_fail(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -730,8 +809,8 @@ def test_signed_without_tt_auth_fail(self): headers={ "x-trace-options": f"sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp}", "x-trace-options-signature": "bad-sig", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -739,19 +818,19 @@ def test_signed_without_tt_auth_fail(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -765,10 +844,7 @@ def test_signed_without_tt_auth_fail(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -779,7 +855,9 @@ def test_signed_without_tt_auth_fail(self): # Verify x-trace-options-response response header present # but only with 'auth' KV assert "x-trace-options-response" in resp.headers - assert "auth=no-signature-key" in resp.headers["x-trace-options-response"] + assert ( + "auth=no-signature-key" in resp.headers["x-trace-options-response"] + ) assert "ignored" not in resp.headers["x-trace-options-response"] # Verify no spans exported @@ -807,36 +885,37 @@ def test_signed_with_tt_auth_fail_bad_ts(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"key", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "key", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): - x_trace_options = f"trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp-5*60-1}" - x_trace_options_signature = hmac.new(str.encode("key"), str.encode(x_trace_options), hashlib.sha1).hexdigest() + x_trace_options = f"trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp - 5 * 60 - 1}" + x_trace_options_signature = hmac.new( + str.encode("key"), str.encode(x_trace_options), hashlib.sha1 + ).hexdigest() # Request to instrumented app with headers resp = self.client.get( "/test_trace/", headers={ "x-trace-options": x_trace_options, "x-trace-options-signature": x_trace_options_signature, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -844,19 +923,19 @@ def test_signed_with_tt_auth_fail_bad_ts(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -870,7 +949,7 @@ def test_signed_with_tt_auth_fail_bad_ts(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -909,29 +988,29 @@ def test_signed_without_tt_auth_fail_bad_ts(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"key", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "key", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): x_trace_options = f"sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo;ts={timestamp - 5 * 60 - 1}" - x_trace_options_signature = hmac.new(str.encode("key"), str.encode(x_trace_options), - hashlib.sha1).hexdigest() + x_trace_options_signature = hmac.new( + str.encode("key"), str.encode(x_trace_options), hashlib.sha1 + ).hexdigest() # Request to instrumented app with headers resp = self.client.get( @@ -939,8 +1018,8 @@ def test_signed_without_tt_auth_fail_bad_ts(self): headers={ "x-trace-options": x_trace_options, "x-trace-options-signature": x_trace_options_signature, - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -948,19 +1027,19 @@ def test_signed_without_tt_auth_fail_bad_ts(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -974,10 +1053,7 @@ def test_signed_without_tt_auth_fail_bad_ts(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -1016,23 +1092,22 @@ def test_signed_missing_xtraceoptions_header(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"key", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "key", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -1041,27 +1116,27 @@ def test_signed_missing_xtraceoptions_header(self): "/test_trace/", headers={ "x-trace-options-signature": "good-sig-but-no-ts", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) # Verify some-header was not altered by instrumentation try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -1076,10 +1151,7 @@ def test_signed_missing_xtraceoptions_header(self): # where value will be new_span_id and new_trace_flags. # There should be no `xtrace_options_response` key because there is # no trigger-trace in the extracted x-trace-options header. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask diff --git a/tests/integration/test_unsigned_tt.py b/tests/integration/test_unsigned_tt.py index 972b68e81..431514890 100644 --- a/tests/integration/test_unsigned_tt.py +++ b/tests/integration/test_unsigned_tt.py @@ -4,14 +4,15 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -import re import json +import re import time +from unittest import mock from opentelemetry import trace as trace_api -from unittest import mock from solarwinds_apm.oboe.settings import LocalSettings, TracingMode + from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes @@ -46,23 +47,22 @@ def test_unsigned_with_tt_sampled(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): @@ -71,8 +71,8 @@ def test_unsigned_with_tt_sampled(self): "/test_trace/", headers={ "x-trace-options": "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -80,19 +80,19 @@ def test_unsigned_with_tt_sampled(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -106,7 +106,7 @@ def test_unsigned_with_tt_sampled(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -117,7 +117,10 @@ def test_unsigned_with_tt_sampled(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers assert "trigger-trace=ok" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify spans exported: service entry (root) + outgoing request (child with local parent) spans = self.memory_exporter.get_finished_spans() @@ -133,12 +136,21 @@ def test_unsigned_with_tt_sampled(self): # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_server.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check root span attributes # :present: @@ -151,28 +163,43 @@ def test_unsigned_with_tt_sampled(self): # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes - assert span_server.attributes["SWKeys"] == "check-id:check-1013,website-id:booking-demo" + assert ( + span_server.attributes["SWKeys"] + == "check-id:check-1013,website-id:booking-demo" + ) assert "custom-awesome-key" in span_server.attributes assert span_server.attributes["custom-awesome-key"] == "foo" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "this-will-be-ignored" not in span_server.attributes # Check client span tracestate has `xtrace_options_response` key but no `sw` key # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_client.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check outgoing request span attributes # :absent: @@ -182,18 +209,21 @@ def test_unsigned_with_tt_sampled(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-awesome-key" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-awesome-key" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "this-will-be-ignored" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id def test_unsigned_with_tt_not_sampled_rate_exceeded(self): """ @@ -215,23 +245,22 @@ def test_unsigned_with_tt_not_sampled_rate_exceeded(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":0, - "TriggerStrictBucketRate":0, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 0, + "TriggerStrictBucketRate": 0, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -240,8 +269,8 @@ def test_unsigned_with_tt_not_sampled_rate_exceeded(self): "/test_trace/", headers={ "x-trace-options": "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -249,19 +278,19 @@ def test_unsigned_with_tt_not_sampled_rate_exceeded(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -275,7 +304,7 @@ def test_unsigned_with_tt_not_sampled_rate_exceeded(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -285,8 +314,14 @@ def test_unsigned_with_tt_not_sampled_rate_exceeded(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers - assert "trigger-trace=rate-exceeded" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=rate-exceeded" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify no spans exported spans = self.memory_exporter.get_finished_spans() @@ -308,61 +343,64 @@ def test_unsigned_with_tt_not_sampled_tt_disabled(self): resp = None # Mock JSON read to guarantee sample decision timestamp = int(time.time()) - with mock.patch( + with ( + mock.patch( target="solarwinds_apm.oboe.sampler.Sampler.local_settings", - return_value=LocalSettings(tracing_mode=TracingMode.ALWAYS, trigger_mode=False) - ): - with mock.patch( + return_value=LocalSettings( + tracing_mode=TracingMode.ALWAYS, trigger_mode=False + ), + ), + mock.patch( target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], - ): - # Request to instrumented app with headers - resp = self.client.get( - "/test_trace/", - headers={ - "x-trace-options": "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", - "some-header": "some-value" - } - ) + ), + ): + # Request to instrumented app with headers + resp = self.client.get( + "/test_trace/", + headers={ + "x-trace-options": "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", + "some-header": "some-value", + }, + ) resp_json = json.loads(resp.data) # Verify some-header was not altered by instrumentation try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -376,7 +414,7 @@ def test_unsigned_with_tt_not_sampled_tt_disabled(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -386,8 +424,14 @@ def test_unsigned_with_tt_not_sampled_tt_disabled(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers - assert "trigger-trace=trigger-tracing-disabled" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=trigger-tracing-disabled" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify no spans exported spans = self.memory_exporter.get_finished_spans() @@ -417,23 +461,22 @@ def test_unsigned_without_tt_sampled(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): @@ -442,8 +485,8 @@ def test_unsigned_without_tt_sampled(self): "/test_trace/", headers={ "x-trace-options": "sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -451,19 +494,19 @@ def test_unsigned_without_tt_sampled(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -477,10 +520,7 @@ def test_unsigned_without_tt_sampled(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format( - new_span_id, - new_trace_flags, - ) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -490,8 +530,14 @@ def test_unsigned_without_tt_sampled(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers - assert "trigger-trace=not-requested" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=not-requested" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify spans exported: service entry (root) + outgoing request (child with local parent) spans = self.memory_exporter.get_finished_spans() @@ -507,13 +553,22 @@ def test_unsigned_without_tt_sampled(self): # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####not-requested;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####not-requested;ignored####this-will-be-ignored", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state actual_trace_state = span_server.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check root span attributes # :present: @@ -524,14 +579,20 @@ def test_unsigned_without_tt_sampled(self): # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # TriggeredTrace, because trigger-trace not in otel context # the ignored value in the x-trace-options-header - assert all(attr_key in span_server.attributes for attr_key in self.SW_SETTINGS_KEYS) + assert all( + attr_key in span_server.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) assert span_server.attributes["BucketCapacity"] == 2 assert span_server.attributes["BucketRate"] == 1 assert span_server.attributes["SampleRate"] == 1000000 assert span_server.attributes["SampleSource"] == 6 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes - assert span_server.attributes["SWKeys"] == "check-id:check-1013,website-id:booking-demo" + assert ( + span_server.attributes["SWKeys"] + == "check-id:check-1013,website-id:booking-demo" + ) assert "custom-awesome-key" in span_server.attributes assert span_server.attributes["custom-awesome-key"] == "foo" assert "TriggeredTrace" not in span_server.attributes @@ -541,12 +602,21 @@ def test_unsigned_without_tt_sampled(self): # because no valid parent context. # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####not-requested;ignored####this-will-be-ignored"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####not-requested;ignored####this-will-be-ignored", + ), + ] + ) actual_trace_state = span_client.context.trace_state - assert actual_trace_state.get("sw") == expected_trace_state.get("sw") # both None - assert actual_trace_state.get("xtrace_options_response") == expected_trace_state.get("xtrace_options_response") + assert actual_trace_state.get("sw") == expected_trace_state.get( + "sw" + ) # both None + assert actual_trace_state.get( + "xtrace_options_response" + ) == expected_trace_state.get("xtrace_options_response") # Check outgoing request span attributes # :absent: @@ -556,18 +626,21 @@ def test_unsigned_without_tt_sampled(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-awesome-key" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-awesome-key" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "this-will-be-ignored" not in span_client.attributes # Check span_id of the outgoing request span (client span) matches # the span_id portion in the outgoing tracestate header, which # is stored in the test app's response body (new_span_id). # Note: context.span_id needs a 16-byte hex conversion first. - assert "{:016x}".format(span_client.context.span_id) == new_span_id + assert f"{span_client.context.span_id:016x}" == new_span_id def test_unsigned_without_tt_not_sampled_rate_exceeded(self): """ @@ -590,23 +663,22 @@ def test_unsigned_without_tt_not_sampled_rate_exceeded(self): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":0 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 0, } ], ): @@ -615,8 +687,8 @@ def test_unsigned_without_tt_not_sampled_rate_exceeded(self): "/test_trace/", headers={ "x-trace-options": "sw-keys=check-id:check-1013,website-id:booking-demo;this-will-be-ignored;custom-awesome-key=foo", - "some-header": "some-value" - } + "some-header": "some-value", + }, ) resp_json = json.loads(resp.data) @@ -624,19 +696,19 @@ def test_unsigned_without_tt_not_sampled_rate_exceeded(self): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") # Verify trace context injected into test app's outgoing postman-echo call # (added to Flask app's response data) includes: # - traceparent with a trace_id, span_id, and trace_flags for do_sample # - tracestate with same span_id and trace_flags for do_sample assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_trace_id = traceparent_re_result.group(2) @@ -650,7 +722,7 @@ def test_unsigned_without_tt_not_sampled_rate_exceeded(self): # In this test we know tracestate will have `sw` # with new_span_id and new_trace_flags. # `xtrace_options_response` is not propagated. - assert resp_json["tracestate"] == "sw={}-{}".format(new_span_id, new_trace_flags) + assert resp_json["tracestate"] == f"sw={new_span_id}-{new_trace_flags}" # Verify x-trace response header has same trace_id # though it will have different span ID because of Flask @@ -660,8 +732,14 @@ def test_unsigned_without_tt_not_sampled_rate_exceeded(self): # Verify x-trace-options-response response header present assert "x-trace-options-response" in resp.headers - assert "trigger-trace=not-requested" in resp.headers["x-trace-options-response"] - assert "ignored=this-will-be-ignored" in resp.headers["x-trace-options-response"] + assert ( + "trigger-trace=not-requested" + in resp.headers["x-trace-options-response"] + ) + assert ( + "ignored=this-will-be-ignored" + in resp.headers["x-trace-options-response"] + ) # Verify no spans exported spans = self.memory_exporter.get_finished_spans() diff --git a/tests/integration/test_xtraceoptions_validation.py b/tests/integration/test_xtraceoptions_validation.py index 897750362..fbd49fa96 100644 --- a/tests/integration/test_xtraceoptions_validation.py +++ b/tests/integration/test_xtraceoptions_validation.py @@ -4,12 +4,12 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -import re import json +import re import time +from unittest import mock from opentelemetry import trace as trace_api -from unittest import mock from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes @@ -35,23 +35,22 @@ def get_response(self, headers=None): target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", return_value=[ { - "arguments": - { - "BucketCapacity":2, - "BucketRate":1, - "MetricsFlushInterval":60, - "SignatureKey":"", - "TriggerRelaxedBucketCapacity":4, - "TriggerRelaxedBucketRate":3, - "TriggerStrictBucketCapacity":6, - "TriggerStrictBucketRate":5, - }, - "flags":"SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", - "layer":"", - "timestamp":timestamp, - "ttl":120, - "type":0, - "value":1000000 + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, } ], ): @@ -65,12 +64,12 @@ def get_response(self, headers=None): def get_new_span_id_and_trace_flags(self, resp_json): """Get new_span_id and new_trace_flagsfrom resp_json's traceparent""" assert "traceparent" in resp_json - _TRACEPARENT_HEADER_FORMAT = ( + _traceparent_header_format = ( "^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$" ) - _TRACEPARENT_HEADER_FORMAT_RE = re.compile(_TRACEPARENT_HEADER_FORMAT) + _traceparent_header_format_re = re.compile(_traceparent_header_format) traceparent_re_result = re.search( - _TRACEPARENT_HEADER_FORMAT_RE, + _traceparent_header_format_re, resp_json["traceparent"], ) new_span_id = traceparent_re_result.group(3) @@ -84,13 +83,13 @@ def check_some_header_ok(self, resp_json): try: assert resp_json["incoming-headers"]["some-header"] == "some-value" except KeyError as e: - self.fail("KeyError was raised at incoming-headers check: {}".format(e)) + self.fail(f"KeyError was raised at incoming-headers check: {e}") def test_remove_leading_trailing_spaces(self): resp_json = self.get_response( { "x-trace-options": " trigger-trace ; custom-something=value; custom-OtherThing = other val ; sw-keys = 029734wr70:9wqj21,0d9j1 ; ts = 12345 ; foo = bar ", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -113,9 +112,14 @@ def test_remove_leading_trailing_spaces(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####foo", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -130,10 +134,13 @@ def test_remove_leading_trailing_spaces(self): # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace print(f"span_server.attributes = {span_server.attributes}") - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes assert span_server.attributes["SWKeys"] == "029734wr70:9wqj21,0d9j1" assert "custom-something" in span_server.attributes @@ -141,15 +148,20 @@ def test_remove_leading_trailing_spaces(self): assert "custom-OtherThing" in span_server.attributes assert span_server.attributes["custom-OtherThing"] == "other val" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "foo" not in span_server.attributes # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####foo", + ), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -160,19 +172,22 @@ def test_remove_leading_trailing_spaces(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-something" in span_client.attributes - assert not "custom-OtherThing" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-something" not in span_client.attributes + assert "custom-OtherThing" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "foo" not in span_client.attributes def test_handle_sequential_semicolons(self): resp_json = self.get_response( { "x-trace-options": ";foo=bar;;;custom-something=value_thing;;sw-keys=02973r70:1b2a3;;;;custom-key=val;ts=12345;;;;;;;trigger-trace;;;", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -195,9 +210,14 @@ def test_handle_sequential_semicolons(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####foo", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -211,10 +231,13 @@ def test_handle_sequential_semicolons(self): # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes assert span_server.attributes["SWKeys"] == "02973r70:1b2a3" assert "custom-something" in span_server.attributes @@ -222,15 +245,20 @@ def test_handle_sequential_semicolons(self): assert "custom-key" in span_server.attributes assert span_server.attributes["custom-key"] == "val" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "foo" not in span_server.attributes # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####foo"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####foo", + ), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -241,19 +269,22 @@ def test_handle_sequential_semicolons(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-something" in span_client.attributes - assert not "custom-key" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-something" not in span_client.attributes + assert "custom-key" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "foo" not in span_client.attributes def test_keep_first_of_repeated_key(self): resp_json = self.get_response( { "x-trace-options": "custom-something=keep_this_0;sw-keys=keep_this;sw-keys=029734wrqj21,0d9;custom-something=otherval;trigger-trace", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -276,9 +307,14 @@ def test_keep_first_of_repeated_key(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####sw-keys....custom-something"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####sw-keys....custom-something", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -292,24 +328,32 @@ def test_keep_first_of_repeated_key(self): # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes assert span_server.attributes["SWKeys"] == "keep_this" assert "custom-something" in span_server.attributes assert span_server.attributes["custom-something"] == "keep_this_0" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "foo" not in span_server.attributes # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####sw-keys....custom-something"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####sw-keys....custom-something", + ), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -320,18 +364,21 @@ def test_keep_first_of_repeated_key(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-something" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-something" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "foo" not in span_client.attributes def test_keep_values_with_equals_signs(self): resp_json = self.get_response( { "x-trace-options": "trigger-trace;custom-something=value_thing=4;custom-OtherThing=other val;sw-keys=g049sj345=0spd", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -354,9 +401,11 @@ def test_keep_values_with_equals_signs(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ("xtrace_options_response", "trigger-trace####ok"), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -370,10 +419,13 @@ def test_keep_values_with_equals_signs(self): # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes assert span_server.attributes["SWKeys"] == "g049sj345=0spd" assert "custom-something" in span_server.attributes @@ -381,15 +433,17 @@ def test_keep_values_with_equals_signs(self): assert "custom-OtherThing" in span_server.attributes assert span_server.attributes["custom-OtherThing"] == "other val" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "foo" not in span_server.attributes # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ("xtrace_options_response", "trigger-trace####ok"), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -400,19 +454,22 @@ def test_keep_values_with_equals_signs(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-something" in span_client.attributes - assert not "custom-OtherThing" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-something" not in span_client.attributes + assert "custom-OtherThing" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "foo" not in span_client.attributes def test_ignore_tt_with_value(self): resp_json = self.get_response( { "x-trace-options": "trigger-trace=1;custom-something=value_thing=4;custom-OtherThing=other val;sw-keys=g049sj345=0spd", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -435,9 +492,14 @@ def test_ignore_tt_with_value(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####not-requested;ignored####trigger-trace"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####not-requested;ignored####trigger-trace", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -451,10 +513,13 @@ def test_ignore_tt_with_value(self): # TriggeredTrace, because no valid trigger-trace in otel context # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 2 assert span_server.attributes["BucketRate"] == 1 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes assert span_server.attributes["SWKeys"] == "g049sj345=0spd" assert "custom-something" in span_server.attributes @@ -466,9 +531,14 @@ def test_ignore_tt_with_value(self): # Check client span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####not-requested;ignored####trigger-trace"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####not-requested;ignored####trigger-trace", + ), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -479,18 +549,21 @@ def test_ignore_tt_with_value(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-something" in span_client.attributes - assert not "custom-OtherThing" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-something" not in span_client.attributes + assert "custom-OtherThing" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes def test_single_quotes_ok(self): resp_json = self.get_response( { "x-trace-options": "trigger-trace;custom-foo='bar;bar';custom-bar=foo", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -513,9 +586,14 @@ def test_single_quotes_ok(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####bar'"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####bar'", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -529,25 +607,33 @@ def test_single_quotes_ok(self): # SWKeys, because not included in xtraceoptions in otel context # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" not in span_server.attributes assert "custom-foo" in span_server.attributes assert span_server.attributes["custom-foo"] == "'bar" assert "custom-bar" in span_server.attributes assert span_server.attributes["custom-bar"] == "foo" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "bar'" not in span_server.attributes # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####bar'"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####bar'", + ), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -558,19 +644,22 @@ def test_single_quotes_ok(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-foo" in span_client.attributes - assert not "custom-bar" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-foo" not in span_client.attributes + assert "custom-bar" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "bar'" not in span_client.attributes def test_multiple_missing_values_and_semis(self): resp_json = self.get_response( { "x-trace-options": ";trigger-trace;custom-something=value_thing;sw-keys=02973r70:9wqj21,0d9j1;1;2;3;4;5;=custom-key=val?;=", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -593,9 +682,14 @@ def test_multiple_missing_values_and_semis(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####1....2....3....4....5"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####1....2....3....4....5", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -609,16 +703,19 @@ def test_multiple_missing_values_and_semis(self): # sw.tracestate_parent_id, because cannot be set at root nor without attributes at decision # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" in span_server.attributes assert span_server.attributes["SWKeys"] == "02973r70:9wqj21,0d9j1" assert "custom-something" in span_server.attributes assert span_server.attributes["custom-something"] == "value_thing" assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "custom-key" not in span_server.attributes assert "1" not in span_server.attributes assert "2" not in span_server.attributes @@ -629,9 +726,14 @@ def test_multiple_missing_values_and_semis(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####1....2....3....4....5"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####1....2....3....4....5", + ), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -642,12 +744,15 @@ def test_multiple_missing_values_and_semis(self): # custom-*, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "custom-something" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes - assert not "custom-key" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "custom-something" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes + assert "custom-key" not in span_client.attributes assert "1" not in span_server.attributes assert "2" not in span_server.attributes assert "3" not in span_server.attributes @@ -658,7 +763,7 @@ def test_custom_key_spaces_not_allowed(self): resp_json = self.get_response( { "x-trace-options": "trigger-trace;custom- key=this_is_bad;custom-key 7=this_is_bad_too", - "some-header": "some-value" + "some-header": "some-value", } ) self.check_some_header_ok(resp_json) @@ -681,9 +786,14 @@ def test_custom_key_spaces_not_allowed(self): # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####custom- key....custom-key 7"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####custom- key....custom-key 7", + ), + ] + ) assert span_server.context.trace_state == expected_trace_state # Check root span attributes @@ -696,22 +806,30 @@ def test_custom_key_spaces_not_allowed(self): # SWKeys, because not included in xtraceoptions in otel context # the ignored value in the x-trace-options-header # SampleRate, SampleSource in attributes, because it is a trigger trace - assert all(attr_key in span_server.attributes for attr_key in ["BucketCapacity", "BucketRate"]) + assert all( + attr_key in span_server.attributes + for attr_key in ["BucketCapacity", "BucketRate"] + ) assert span_server.attributes["BucketCapacity"] == 6 assert span_server.attributes["BucketRate"] == 5 - assert not "sw.tracestate_parent_id" in span_server.attributes + assert "sw.tracestate_parent_id" not in span_server.attributes assert "SWKeys" not in span_server.attributes assert "TriggeredTrace" in span_server.attributes - assert span_server.attributes["TriggeredTrace"] == True + assert span_server.attributes["TriggeredTrace"] assert "custom- key" not in span_server.attributes assert "custom-key 7" not in span_server.attributes # Check root span tracestate has `xtrace_options_response` key # SWO APM uses TraceState to stash the trigger trace response so it's available # at the time of custom injecting the x-trace-options-response header. - expected_trace_state = trace_api.TraceState([ - ("xtrace_options_response", "trigger-trace####ok;ignored####custom- key....custom-key 7"), - ]) + expected_trace_state = trace_api.TraceState( + [ + ( + "xtrace_options_response", + "trigger-trace####ok;ignored####custom- key....custom-key 7", + ), + ] + ) assert span_client.context.trace_state == expected_trace_state # Check outgoing request span attributes @@ -721,9 +839,12 @@ def test_custom_key_spaces_not_allowed(self): # SWKeys, because only written for service entry spans # TriggeredTrace, because only written for service entry spans # the ignored value in the x-trace-options-header - assert not any(attr_key in span_client.attributes for attr_key in self.SW_SETTINGS_KEYS) - assert not "sw.tracestate_parent_id" in span_client.attributes - assert not "SWKeys" in span_client.attributes - assert not "TriggeredTrace" in span_client.attributes + assert not any( + attr_key in span_client.attributes + for attr_key in self.SW_SETTINGS_KEYS + ) + assert "sw.tracestate_parent_id" not in span_client.attributes + assert "SWKeys" not in span_client.attributes + assert "TriggeredTrace" not in span_client.attributes assert "custom- key" not in span_server.attributes assert "custom-key 7" not in span_server.attributes diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 9167e547e..c3eb85d0c 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -2,4 +2,4 @@ # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index c6be9f4df..4e82b4e3c 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -29,29 +29,19 @@ def patch_set_name( return_value="foo", ) - mock_context = mocker.patch( - "solarwinds_apm.api.context" - ) + mock_context = mocker.patch("solarwinds_apm.api.context") mock_current_span = mocker.Mock(context=mock_context) - mock_current_span.configure_mock( - **{ - "set_attribute": mocker.Mock() - } - ) + mock_current_span.configure_mock(set_attribute=mocker.Mock()) mock_get_fn = mocker.Mock(return_value=None) if span_ready: mock_get_fn = mocker.Mock(return_value=mock_current_span) - - mock_context.configure_mock( - **{ - "get_value": mock_get_fn - } - ) + + mock_context.configure_mock(get_value=mock_get_fn) return mock_context, mock_current_span def test_empty_string(self, mocker): mock_context, mock_current_span = self.patch_set_name(mocker) - assert set_transaction_name("") == False + assert not set_transaction_name("") mock_context.get_value.assert_not_called() mock_current_span.set_attribute.assert_not_called() @@ -59,23 +49,30 @@ def test_agent_not_enabled_noop_tracer_provider(self, mocker): mock_context, mock_current_span = self.patch_set_name(mocker) mocker.patch( "solarwinds_apm.api.get_tracer_provider", - return_value=NoOpTracerProvider() + return_value=NoOpTracerProvider(), ) - assert set_transaction_name("foo") == True + assert set_transaction_name("foo") mock_context.get_value.assert_not_called() mock_current_span.set_attribute.assert_not_called() def test_span_not_started(self, mocker): - mock_context, mock_current_span = self.patch_set_name(mocker, span_ready=False) - assert set_transaction_name("foo") == False + mock_context, mock_current_span = self.patch_set_name( + mocker, span_ready=False + ) + assert not set_transaction_name("foo") mock_context.get_value.assert_called_once() mock_current_span.set_attribute.assert_not_called() def test_agent_enabled(self, mocker): mock_context, mock_current_span = self.patch_set_name(mocker) - assert set_transaction_name("bar") == True - mock_context.get_value.assert_called_once_with("sw-current-trace-entry-span") - mock_current_span.set_attribute.assert_called_once_with("sw.transaction", "bar") + assert set_transaction_name("bar") + mock_context.get_value.assert_called_once_with( + "sw-current-trace-entry-span" + ) + mock_current_span.set_attribute.assert_called_once_with( + "sw.transaction", "bar" + ) + class TestSolarWindsReady: def test_parentbasedsw_sampler_ready(self, mocker): @@ -84,22 +81,19 @@ def get_side_effect(param): return "foo:bar" else: return "foo" + mock_apmconfig = mocker.Mock(spec=SolarWindsApmConfig) mock_apmconfig.service_key = "foo:bar" mock_apmconfig.configure_mock( - **{ - "agent_enabled": True, - "get": mocker.Mock(side_effect=get_side_effect), - "service_name": "foo-service", - "is_lambda": False, - } + agent_enabled=True, + get=mocker.Mock(side_effect=get_side_effect), + service_name="foo-service", + is_lambda=False, ) mock_http_sampler = mocker.Mock(spec=HttpSampler) mock_http_sampler.configure_mock( - **{ - "wait_until_ready": mocker.Mock(return_value=True) - } + wait_until_ready=mocker.Mock(return_value=True) ) mocker.patch( "solarwinds_apm.sampler.HttpSampler", @@ -107,16 +101,12 @@ def get_side_effect(param): ) mock_sampler = ParentBasedSwSampler(mock_apmconfig) mock_tracer_provider = mocker.Mock(spec=SolarwindsTracerProvider) - mock_tracer_provider.configure_mock( - **{ - "sampler": mock_sampler, - } - ) + mock_tracer_provider.configure_mock(sampler=mock_sampler) mocker.patch( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == True + assert solarwinds_ready() def test_parentbasedsw_not_ready(self, mocker): def get_side_effect(param): @@ -124,22 +114,19 @@ def get_side_effect(param): return "foo:bar" else: return "foo" + mock_apmconfig = mocker.Mock(spec=SolarWindsApmConfig) mock_apmconfig.service_key = "foo:bar" mock_apmconfig.configure_mock( - **{ - "agent_enabled": True, - "get": mocker.Mock(side_effect=get_side_effect), - "service_name": "foo-service", - "is_lambda": False, - } + agent_enabled=True, + get=mocker.Mock(side_effect=get_side_effect), + service_name="foo-service", + is_lambda=False, ) mock_http_sampler = mocker.Mock(spec=HttpSampler) mock_http_sampler.configure_mock( - **{ - "wait_until_ready": mocker.Mock(return_value=False) - } + wait_until_ready=mocker.Mock(return_value=False) ) mocker.patch( "solarwinds_apm.sampler.HttpSampler", @@ -147,106 +134,74 @@ def get_side_effect(param): ) mock_sampler = ParentBasedSwSampler(mock_apmconfig) mock_tracer_provider = mocker.Mock(spec=SolarwindsTracerProvider) - mock_tracer_provider.configure_mock( - **{ - "sampler": mock_sampler, - } - ) + mock_tracer_provider.configure_mock(sampler=mock_sampler) mocker.patch( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == False + assert not solarwinds_ready() def test_http_sampler_ready(self, mocker): mock_sampler = mocker.Mock(spec=HttpSampler) mock_sampler.configure_mock( - **{ - "wait_until_ready": mocker.Mock(return_value=True) - } + wait_until_ready=mocker.Mock(return_value=True) ) mock_tracer_provider = mocker.Mock(spec=SolarwindsTracerProvider) - mock_tracer_provider.configure_mock( - **{ - "sampler": mock_sampler, - } - ) + mock_tracer_provider.configure_mock(sampler=mock_sampler) mocker.patch( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == True + assert solarwinds_ready() def test_http_sampler_not_ready(self, mocker): mock_sampler = mocker.Mock(spec=HttpSampler) mock_sampler.configure_mock( - **{ - "wait_until_ready": mocker.Mock(return_value=False) - } + wait_until_ready=mocker.Mock(return_value=False) ) mock_tracer_provider = mocker.Mock(spec=SolarwindsTracerProvider) - mock_tracer_provider.configure_mock( - **{ - "sampler": mock_sampler, - } - ) + mock_tracer_provider.configure_mock(sampler=mock_sampler) mocker.patch( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == False + assert not solarwinds_ready() def test_json_sampler_ready(self, mocker): mock_sampler = mocker.Mock(spec=JsonSampler) mock_sampler.configure_mock( - **{ - "wait_until_ready": mocker.Mock(return_value=True) - } + wait_until_ready=mocker.Mock(return_value=True) ) mock_tracer_provider = mocker.Mock(spec=SolarwindsTracerProvider) - mock_tracer_provider.configure_mock( - **{ - "sampler": mock_sampler, - } - ) + mock_tracer_provider.configure_mock(sampler=mock_sampler) mocker.patch( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == True + assert solarwinds_ready() def test_json_sampler_not_ready(self, mocker): mock_sampler = mocker.Mock(spec=JsonSampler) mock_sampler.configure_mock( - **{ - "wait_until_ready": mocker.Mock(return_value=False) - } + wait_until_ready=mocker.Mock(return_value=False) ) mock_tracer_provider = mocker.Mock(spec=SolarwindsTracerProvider) - mock_tracer_provider.configure_mock( - **{ - "sampler": mock_sampler, - } - ) + mock_tracer_provider.configure_mock(sampler=mock_sampler) mocker.patch( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == False + assert not solarwinds_ready() def test_other_sampler(self, mocker): mock_sampler = mocker.Mock(spec=ParentBased) mock_tracer_provider = mocker.Mock(spec=SolarwindsTracerProvider) - mock_tracer_provider.configure_mock( - **{ - "sampler": mock_sampler, - } - ) + mock_tracer_provider.configure_mock(sampler=mock_sampler) mocker.patch( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == False + assert not solarwinds_ready() def test_other_tracer_provider(self, mocker): mock_tracer_provider = mocker.Mock(spec=TracerProvider) @@ -254,4 +209,4 @@ def test_other_tracer_provider(self, mocker): "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracer_provider, ) - assert solarwinds_ready() == False + assert not solarwinds_ready() diff --git a/tests/unit/test_apm_config/conftest.py b/tests/unit/test_apm_config/conftest.py new file mode 100644 index 000000000..68dbd6482 --- /dev/null +++ b/tests/unit/test_apm_config/conftest.py @@ -0,0 +1,24 @@ +"""Pytest configuration and fixtures for test_apm_config tests.""" + +# Import all fixtures to make them available to tests +from .fixtures.cnf_dict import ( + fixture_cnf_dict, + fixture_cnf_dict_enabled_false, + fixture_cnf_dict_enabled_false_mixed_case, +) +from .fixtures.cnf_file import ( + fixture_cnf_file, + fixture_cnf_file_invalid_json, +) +from .fixtures.env_vars import ( + fixture_mock_env_vars, +) + +__all__ = [ + "fixture_cnf_dict", + "fixture_cnf_dict_enabled_false", + "fixture_cnf_dict_enabled_false_mixed_case", + "fixture_cnf_file", + "fixture_cnf_file_invalid_json", + "fixture_mock_env_vars", +] diff --git a/tests/unit/test_apm_config/fixtures/cnf_dict.py b/tests/unit/test_apm_config/fixtures/cnf_dict.py index 95d6b01b3..bba0e5358 100644 --- a/tests/unit/test_apm_config/fixtures/cnf_dict.py +++ b/tests/unit/test_apm_config/fixtures/cnf_dict.py @@ -1,5 +1,6 @@ import pytest + @pytest.fixture def fixture_cnf_dict(): return { @@ -13,6 +14,7 @@ def fixture_cnf_dict(): "exportLogsEnabled": True, } + @pytest.fixture def fixture_cnf_dict_enabled_false(): return { @@ -26,6 +28,7 @@ def fixture_cnf_dict_enabled_false(): "exportLogsEnabled": False, } + @pytest.fixture def fixture_cnf_dict_enabled_false_mixed_case(): return { @@ -36,4 +39,4 @@ def fixture_cnf_dict_enabled_false_mixed_case(): "debugLevel": 6, "serviceKey": "not-good-to-put-here:still-could-be-used", "exportLogsEnabled": "fALsE", - } \ No newline at end of file + } diff --git a/tests/unit/test_apm_config/fixtures/cnf_file.py b/tests/unit/test_apm_config/fixtures/cnf_file.py index a0cd27c0e..41be65a26 100644 --- a/tests/unit/test_apm_config/fixtures/cnf_file.py +++ b/tests/unit/test_apm_config/fixtures/cnf_file.py @@ -1,5 +1,6 @@ import pytest + @pytest.fixture def fixture_cnf_file(mocker): read_data = '{"foo": "bar"}' @@ -7,6 +8,7 @@ def fixture_cnf_file(mocker): builtin_open = "builtins.open" mocker.patch(builtin_open, mocked_cnf_file_data) + @pytest.fixture def fixture_cnf_file_invalid_json(mocker): mocked_cnf_file_data = mocker.mock_open(read_data="invalid-foo") diff --git a/tests/unit/test_apm_config/fixtures/env_vars.py b/tests/unit/test_apm_config/fixtures/env_vars.py index e96d24664..50a9432b5 100644 --- a/tests/unit/test_apm_config/fixtures/env_vars.py +++ b/tests/unit/test_apm_config/fixtures/env_vars.py @@ -1,11 +1,16 @@ import os + import pytest from solarwinds_apm.apm_constants import INTL_SWO_DEFAULT_PROPAGATORS + @pytest.fixture(name="mock_env_vars") def fixture_mock_env_vars(mocker): - mocker.patch.dict(os.environ, { - "OTEL_PROPAGATORS": ",".join(INTL_SWO_DEFAULT_PROPAGATORS), - "SW_APM_SERVICE_KEY": "valid:key", - }) \ No newline at end of file + mocker.patch.dict( + os.environ, + { + "OTEL_PROPAGATORS": ",".join(INTL_SWO_DEFAULT_PROPAGATORS), + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) diff --git a/tests/unit/test_apm_config/test_apm_config.py b/tests/unit/test_apm_config/test_apm_config.py index 86a01e99f..e58aafcd9 100644 --- a/tests/unit/test_apm_config/test_apm_config.py +++ b/tests/unit/test_apm_config/test_apm_config.py @@ -10,14 +10,13 @@ import re import pytest - from opentelemetry.sdk.resources import Resource from solarwinds_apm import apm_config from solarwinds_apm.oboe.configuration import Configuration, TransactionSetting # pylint: disable=unused-import -from .fixtures.env_vars import fixture_mock_env_vars + @pytest.fixture def setup_caplog(): @@ -53,7 +52,9 @@ def before_and_after_each(self): old_trustedpath = os.environ.get("SW_APM_TRUSTEDPATH", None) if old_trustedpath: del os.environ["SW_APM_TRUSTEDPATH"] - old_expt_metrics = os.environ.get("SW_APM_EXPORT_METRICS_ENABLED", None) + old_expt_metrics = os.environ.get( + "SW_APM_EXPORT_METRICS_ENABLED", None + ) if old_expt_metrics: del os.environ["SW_APM_EXPORT_METRICS_ENABLED"] @@ -78,168 +79,238 @@ def before_and_after_each(self): def test__default_collector(self, mocker): test_config = apm_config.SolarWindsApmConfig() - assert test_config.get("collector") == apm_config.SolarWindsApmConfig._CONFIG_COLLECTOR_DEFAULT + assert ( + test_config.get("collector") + == apm_config.SolarWindsApmConfig._CONFIG_COLLECTOR_DEFAULT + ) def test__init_collector(self, mocker): - mocker.patch.dict(os.environ, { - "SW_APM_COLLECTOR": "apm.collector.eu-02.cloud.solarwinds.com" - }) + mocker.patch.dict( + os.environ, + {"SW_APM_COLLECTOR": "apm.collector.eu-02.cloud.solarwinds.com"}, + ) test_config = apm_config.SolarWindsApmConfig() - assert test_config.get("collector") == "apm.collector.eu-02.cloud.solarwinds.com" + assert ( + test_config.get("collector") + == "apm.collector.eu-02.cloud.solarwinds.com" + ) def _mock_service_key(self, mocker, service_key): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": service_key, - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": service_key, + }, + ) def test__init_invalid_service_key_format(self, mocker): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "incorrect_format", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "incorrect_format", + }, + ) test_config = apm_config.SolarWindsApmConfig() assert not test_config.agent_enabled assert test_config.service_name == "" assert test_config.get("service_key") == "incorrect_format" - def test__init_invalid_service_key_format_otel_service_name_and_resource_attrs(self, mocker): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "incorrect_format", - "OTEL_SERVICE_NAME": "wont_be_used", - "OTEL_RESOURCE_ATTRIBUTES": "service.name=also_unused" - }) + def test__init_invalid_service_key_format_otel_service_name_and_resource_attrs( + self, mocker + ): + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "incorrect_format", + "OTEL_SERVICE_NAME": "wont_be_used", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=also_unused", + }, + ) test_config = apm_config.SolarWindsApmConfig() assert not test_config.agent_enabled assert test_config.service_name == "" assert test_config.get("service_key") == "incorrect_format" def test__init_valid_service_key_format_agent_enabled_false(self, mocker): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "SW_APM_AGENT_ENABLED": "false", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "SW_APM_AGENT_ENABLED": "false", + }, + ) test_config = apm_config.SolarWindsApmConfig() assert not test_config.agent_enabled assert test_config.service_name == "" - assert test_config.get("service_key") == "service_key_with:sw_service_name" + assert ( + test_config.get("service_key") + == "service_key_with:sw_service_name" + ) def test__init_valid_service_key_format_agent_enabled_true_default( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + }, + ) test_config = apm_config.SolarWindsApmConfig() assert test_config.agent_enabled assert test_config.service_name == "sw_service_name" - assert test_config.get("service_key") == "service_key_with:sw_service_name" + assert ( + test_config.get("service_key") + == "service_key_with:sw_service_name" + ) def test__init_valid_service_key_format_agent_enabled_true_explicit( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "SW_APM_AGENT_ENABLED": "true", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "SW_APM_AGENT_ENABLED": "true", + }, + ) test_config = apm_config.SolarWindsApmConfig() assert test_config.agent_enabled assert test_config.service_name == "sw_service_name" - assert test_config.get("service_key") == "service_key_with:sw_service_name" + assert ( + test_config.get("service_key") + == "service_key_with:sw_service_name" + ) def test__init_valid_service_key_format_otel_service_name( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "OTEL_SERVICE_NAME": "from_otel_env" - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "OTEL_SERVICE_NAME": "from_otel_env", + }, + ) # Explicitly pass Resource.create() to pick up env vars set above test_config = apm_config.SolarWindsApmConfig(Resource.create()) assert test_config.agent_enabled assert test_config.service_name == "from_otel_env" - assert test_config.get("service_key") == "service_key_with:from_otel_env" + assert ( + test_config.get("service_key") == "service_key_with:from_otel_env" + ) def test__init_valid_service_key_format_otel_service_name_and_resource_attrs( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "OTEL_SERVICE_NAME": "from_otel_env", - "OTEL_RESOURCE_ATTRIBUTES": "service.name=also_from_otel_env_unused" - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "OTEL_SERVICE_NAME": "from_otel_env", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=also_from_otel_env_unused", + }, + ) # Otel picks up os mock if Resource.create here (same as default arg) test_config = apm_config.SolarWindsApmConfig(Resource.create()) assert test_config.agent_enabled assert test_config.service_name == "from_otel_env" - assert test_config.get("service_key") == "service_key_with:from_otel_env" + assert ( + test_config.get("service_key") == "service_key_with:from_otel_env" + ) def test__init_valid_service_key_format_otel_resource_attrs( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "OTEL_RESOURCE_ATTRIBUTES": "service.name=also_from_otel_env_used_this_time" - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "OTEL_RESOURCE_ATTRIBUTES": "service.name=also_from_otel_env_used_this_time", + }, + ) # Otel picks up os mock if Resource.create here (same as default arg) test_config = apm_config.SolarWindsApmConfig(Resource.create()) assert test_config.agent_enabled assert test_config.service_name == "also_from_otel_env_used_this_time" - assert test_config.get("service_key") == "service_key_with:also_from_otel_env_used_this_time" + assert ( + test_config.get("service_key") + == "service_key_with:also_from_otel_env_used_this_time" + ) def test__init_valid_service_key_format_empty_otel_service_name( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "OTEL_SERVICE_NAME": "", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "OTEL_SERVICE_NAME": "", + }, + ) # Otel picks up os mock if Resource.create here (same as default arg) test_config = apm_config.SolarWindsApmConfig(Resource.create()) assert test_config.agent_enabled assert test_config.service_name == "sw_service_name" - assert test_config.get("service_key") == "service_key_with:sw_service_name" + assert ( + test_config.get("service_key") + == "service_key_with:sw_service_name" + ) def test__init_valid_service_key_format_empty_otel_service_name_and_resource_attrs( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "OTEL_SERVICE_NAME": "", - "OTEL_RESOURCE_ATTRIBUTES": "", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "OTEL_SERVICE_NAME": "", + "OTEL_RESOURCE_ATTRIBUTES": "", + }, + ) # Otel picks up os mock if Resource.create here (same as default arg) test_config = apm_config.SolarWindsApmConfig(Resource.create()) assert test_config.agent_enabled assert test_config.service_name == "sw_service_name" - assert test_config.get("service_key") == "service_key_with:sw_service_name" + assert ( + test_config.get("service_key") + == "service_key_with:sw_service_name" + ) def test__init_valid_service_key_format_otel_resource_attrs_without_name( self, mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - "OTEL_RESOURCE_ATTRIBUTES": "foo=bar,telemetry.sdk.version=whatever-i-want-baby", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + "OTEL_RESOURCE_ATTRIBUTES": "foo=bar,telemetry.sdk.version=whatever-i-want-baby", + }, + ) # Otel picks up os mock if Resource.create here (same as default arg) test_config = apm_config.SolarWindsApmConfig(Resource.create()) assert test_config.agent_enabled assert test_config.service_name == "sw_service_name" - assert test_config.get("service_key") == "service_key_with:sw_service_name" + assert ( + test_config.get("service_key") + == "service_key_with:sw_service_name" + ) def test__init_custom_transction_names_env_vars(self, mocker): mocker.patch.dict( @@ -267,17 +338,38 @@ def test_mask_service_key_whitespace_key(self, mocker): def test_mask_service_key_invalid_format_no_colon(self, mocker): self._mock_service_key(mocker, "a") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "a" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "a" + ) self._mock_service_key(mocker, "abcd") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd" + ) self._mock_service_key(mocker, "abcde") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd..." + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd..." + ) self._mock_service_key(mocker, "abcdefgh") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd..." + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd..." + ) self._mock_service_key(mocker, "abcd1efgh") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd..." - self._mock_service_key(mocker, "CyUuit1W--8RVmUXX6_cVjTWemaUyBh1ruL0nMPiFdrPo1iiRnO31_pwiUCPzdzv9UMHK6I") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "CyUu..." + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd..." + ) + self._mock_service_key( + mocker, + "CyUuit1W--8RVmUXX6_cVjTWemaUyBh1ruL0nMPiFdrPo1iiRnO31_pwiUCPzdzv9UMHK6I", + ) + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "CyUu..." + ) def test_mask_service_key_less_than_9_char_token( self, @@ -285,23 +377,47 @@ def test_mask_service_key_less_than_9_char_token( mock_env_vars, ): self._mock_service_key(mocker, ":foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == ":foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() == ":foo-bar" + ) self._mock_service_key(mocker, "a:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "a:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() == "a:foo-bar" + ) self._mock_service_key(mocker, "ab:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "ab:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() == "ab:foo-bar" + ) self._mock_service_key(mocker, "abc:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abc:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abc:foo-bar" + ) self._mock_service_key(mocker, "abcd:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd:foo-bar" + ) self._mock_service_key(mocker, "abcde:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcde:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcde:foo-bar" + ) self._mock_service_key(mocker, "abcdef:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcdef:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcdef:foo-bar" + ) self._mock_service_key(mocker, "abcdefg:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcdefg:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcdefg:foo-bar" + ) self._mock_service_key(mocker, "abcdefgh:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcdefgh:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcdefgh:foo-bar" + ) def test_mask_service_key_9_or_more_char_token( self, @@ -309,15 +425,33 @@ def test_mask_service_key_9_or_more_char_token( mock_env_vars, ): self._mock_service_key(mocker, "abcd1efgh:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd...efgh:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd...efgh:foo-bar" + ) self._mock_service_key(mocker, "abcd12efgh:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd...efgh:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd...efgh:foo-bar" + ) self._mock_service_key(mocker, "abcd123efgh:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd...efgh:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd...efgh:foo-bar" + ) self._mock_service_key(mocker, "abcd1234567890efgh:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd...efgh:foo-bar" - self._mock_service_key(mocker, "CyUuit1W--8RVmUXX6_cVjTWemaUyBh1ruL0nMPiFdrPo1iiRnO31_pwiUCPzdzv9UMHK6I:foo-bar") - assert apm_config.SolarWindsApmConfig().mask_service_key() == "CyUu...HK6I:foo-bar" + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "abcd...efgh:foo-bar" + ) + self._mock_service_key( + mocker, + "CyUuit1W--8RVmUXX6_cVjTWemaUyBh1ruL0nMPiFdrPo1iiRnO31_pwiUCPzdzv9UMHK6I:foo-bar", + ) + assert ( + apm_config.SolarWindsApmConfig().mask_service_key() + == "CyUu...HK6I:foo-bar" + ) def test_config_mask_service_key( self, @@ -325,7 +459,12 @@ def test_config_mask_service_key( mock_env_vars, ): self._mock_service_key(mocker, "valid-and-long:key") - assert apm_config.SolarWindsApmConfig()._config_mask_service_key().get("service_key") == "vali...long:key" + assert ( + apm_config.SolarWindsApmConfig() + ._config_mask_service_key() + .get("service_key") + == "vali...long:key" + ) def test_config_lambda( self, @@ -408,28 +547,36 @@ def test_str_lambda_environment( assert "apm.collector" not in result # pylint:disable=unused-argument - def test_set_config_value_invalid_key(self, caplog, setup_caplog, mock_env_vars): + def test_set_config_value_invalid_key( + self, caplog, setup_caplog, mock_env_vars + ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("invalid_key", "foo") assert test_config.get("invalid_key", None) is None assert "Ignore invalid configuration key" in caplog.text # pylint:disable=unused-argument - def test_set_config_value_default_tracing_mode(self, caplog, setup_caplog, mock_env_vars): + def test_set_config_value_default_tracing_mode( + self, caplog, setup_caplog, mock_env_vars + ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("tracing_mode", "not-valid-mode") assert test_config.get("tracing_mode") == -1 assert "Ignore config option" in caplog.text # pylint:disable=unused-argument - def test_set_config_value_default_trigger_trace(self, caplog, setup_caplog, mock_env_vars): + def test_set_config_value_default_trigger_trace( + self, caplog, setup_caplog, mock_env_vars + ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("trigger_trace", "not-valid-mode") assert test_config.get("trigger_trace") == 1 assert "Ignore config option" in caplog.text # pylint:disable=unused-argument - def test_set_config_value_default_debug_level(self, caplog, setup_caplog, mock_env_vars): + def test_set_config_value_default_debug_level( + self, caplog, setup_caplog, mock_env_vars + ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("debug_level", "not-valid-level") assert test_config.get("debug_level") == 2 @@ -439,7 +586,7 @@ def test_set_config_value_default_export_metrics_enabled( self, ): test_config = apm_config.SolarWindsApmConfig() - assert test_config.get("export_metrics_enabled") == True + assert test_config.get("export_metrics_enabled") def test_set_config_value_ignore_export_metrics_enabled( self, @@ -449,8 +596,9 @@ def test_set_config_value_ignore_export_metrics_enabled( ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("export_metrics_enabled", "not-valid") - assert test_config.get("export_metrics_enabled") == True + assert test_config.get("export_metrics_enabled") assert "Ignore config option" in caplog.text + def test_set_config_value_set_export_metrics_enabled_false( self, caplog, @@ -459,7 +607,7 @@ def test_set_config_value_set_export_metrics_enabled_false( ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("export_metrics_enabled", "false") - assert test_config.get("export_metrics_enabled") == False + assert not test_config.get("export_metrics_enabled") assert "Ignore config option" not in caplog.text def test_set_config_value_set_export_metrics_enabled_false_mixed_case( @@ -470,7 +618,7 @@ def test_set_config_value_set_export_metrics_enabled_false_mixed_case( ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("export_metrics_enabled", "fALsE") - assert test_config.get("export_metrics_enabled") == False + assert not test_config.get("export_metrics_enabled") assert "Ignore config option" not in caplog.text def test_set_config_value_set_export_metrics_enabled_true( @@ -481,7 +629,7 @@ def test_set_config_value_set_export_metrics_enabled_true( ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("export_metrics_enabled", "true") - assert test_config.get("export_metrics_enabled") == True + assert test_config.get("export_metrics_enabled") assert "Ignore config option" not in caplog.text def test_set_config_value_set_export_metrics_enabled_true_mixed_case( @@ -492,78 +640,64 @@ def test_set_config_value_set_export_metrics_enabled_true_mixed_case( ): test_config = apm_config.SolarWindsApmConfig() test_config._set_config_value("export_metrics_enabled", "tRUe") - assert test_config.get("export_metrics_enabled") == True + assert test_config.get("export_metrics_enabled") assert "Ignore config option" not in caplog.text def test__update_service_key_name_not_agent_enabled(self): test_config = apm_config.SolarWindsApmConfig() - result = test_config._update_service_key_name( - False, - "foo", - "bar" - ) + result = test_config._update_service_key_name(False, "foo", "bar") assert result == "foo" def test__update_service_key_name_empty_service_name(self): test_config = apm_config.SolarWindsApmConfig() - result = test_config._update_service_key_name( - True, - "foo", - "" - ) + result = test_config._update_service_key_name(True, "foo", "") assert result == "foo" - def test__update_service_key_name_not_agent_enabled_and_empty_service_name(self): + def test__update_service_key_name_not_agent_enabled_and_empty_service_name( + self, + ): test_config = apm_config.SolarWindsApmConfig() - result = test_config._update_service_key_name( - False, - "foo", - "" - ) + result = test_config._update_service_key_name(False, "foo", "") assert result == "foo" def test__update_service_key_name_agent_enabled_and_service_name_ok(self): test_config = apm_config.SolarWindsApmConfig() result = test_config._update_service_key_name( - True, - "valid_key_with:foo-service", - "bar-service" + True, "valid_key_with:foo-service", "bar-service" ) assert result == "valid_key_with:bar-service" - def test__update_service_key_name_agent_enabled_and_service_name_ok_but_service_key_missing(self): + def test__update_service_key_name_agent_enabled_and_service_name_ok_but_service_key_missing( + self, + ): test_config = apm_config.SolarWindsApmConfig() result = test_config._update_service_key_name( - True, - None, - "bar-service" + True, None, "bar-service" ) assert result is None - def test__update_service_key_name_agent_enabled_and_service_name_ok_but_service_key_empty(self): + def test__update_service_key_name_agent_enabled_and_service_name_ok_but_service_key_empty( + self, + ): test_config = apm_config.SolarWindsApmConfig() - result = test_config._update_service_key_name( - True, - "", - "bar-service" - ) + result = test_config._update_service_key_name(True, "", "bar-service") assert result == "" - def test__update_service_key_name_agent_enabled_and_service_name_ok_but_service_key_no_delimiter(self): + def test__update_service_key_name_agent_enabled_and_service_name_ok_but_service_key_no_delimiter( + self, + ): test_config = apm_config.SolarWindsApmConfig() result = test_config._update_service_key_name( - True, - "weird-key-no-delimiter", - "bar-service" + True, "weird-key-no-delimiter", "bar-service" ) assert result == "weird-key-no-delimiter" - def test__update_service_key_name_agent_enabled_and_service_name_ok_service_key_multiple_delimiter(self): + def test__update_service_key_name_agent_enabled_and_service_name_ok_service_key_multiple_delimiter( + self, + ): test_config = apm_config.SolarWindsApmConfig() result = test_config._update_service_key_name( - True, - "weird-key:with:2-delimiters", - "bar-service" + True, "weird-key:with:2-delimiters", "bar-service" ) # Updates everything after first delim assert result == "weird-key:bar-service" @@ -571,9 +705,11 @@ def test__update_service_key_name_agent_enabled_and_service_name_ok_service_key_ def test__validate_log_filepath_none(self, mocker): mocker.patch( "solarwinds_apm.apm_config.Resource.create", - return_value=Resource.get_empty() + return_value=Resource.get_empty(), + ) + mock_exists = mocker.patch( + "solarwinds_apm.apm_config.os.path.exists", return_value=False ) - mock_exists = mocker.patch("solarwinds_apm.apm_config.os.path.exists", return_value=False) mock_makedirs = mocker.patch("solarwinds_apm.apm_config.os.makedirs") test_config = apm_config.SolarWindsApmConfig() @@ -586,9 +722,11 @@ def test__validate_log_filepath_none(self, mocker): def test__validate_log_filepath_no_parent_path(self, mocker): mocker.patch( "solarwinds_apm.apm_config.Resource.create", - return_value=Resource.get_empty() + return_value=Resource.get_empty(), + ) + mock_exists = mocker.patch( + "solarwinds_apm.apm_config.os.path.exists", return_value=False ) - mock_exists = mocker.patch("solarwinds_apm.apm_config.os.path.exists", return_value=False) mock_makedirs = mocker.patch("solarwinds_apm.apm_config.os.makedirs") test_config = apm_config.SolarWindsApmConfig() @@ -601,9 +739,11 @@ def test__validate_log_filepath_no_parent_path(self, mocker): def test__validate_log_filepath_path_exists(self, mocker): mocker.patch( "solarwinds_apm.apm_config.Resource.create", - return_value=Resource.get_empty() + return_value=Resource.get_empty(), + ) + mock_exists = mocker.patch( + "solarwinds_apm.apm_config.os.path.exists", return_value=True ) - mock_exists = mocker.patch("solarwinds_apm.apm_config.os.path.exists", return_value=True) mock_makedirs = mocker.patch("solarwinds_apm.apm_config.os.makedirs") test_config = apm_config.SolarWindsApmConfig() @@ -616,9 +756,11 @@ def test__validate_log_filepath_path_exists(self, mocker): def test__validate_log_filepath_create_path(self, mocker): mocker.patch( "solarwinds_apm.apm_config.Resource.create", - return_value=Resource.get_empty() + return_value=Resource.get_empty(), + ) + mock_exists = mocker.patch( + "solarwinds_apm.apm_config.os.path.exists", return_value=False ) - mock_exists = mocker.patch("solarwinds_apm.apm_config.os.path.exists", return_value=False) mock_makedirs = mocker.patch("solarwinds_apm.apm_config.os.makedirs") test_config = apm_config.SolarWindsApmConfig() @@ -631,12 +773,14 @@ def test__validate_log_filepath_create_path(self, mocker): def test__validate_log_filepath_cannot_create_reset_settings(self, mocker): mocker.patch( "solarwinds_apm.apm_config.Resource.create", - return_value=Resource.get_empty() + return_value=Resource.get_empty(), + ) + mock_exists = mocker.patch( + "solarwinds_apm.apm_config.os.path.exists", return_value=False ) - mock_exists = mocker.patch("solarwinds_apm.apm_config.os.path.exists", return_value=False) mock_makedirs = mocker.patch( "solarwinds_apm.apm_config.os.makedirs", - side_effect=FileNotFoundError("mock error") + side_effect=FileNotFoundError("mock error"), ) test_config = apm_config.SolarWindsApmConfig() @@ -678,10 +822,12 @@ def test_convert_to_bool_str_false_mixed_case(self): test_config = apm_config.SolarWindsApmConfig() assert not test_config.convert_to_bool("fAlSE") + @pytest.fixture def apm(): return apm_config.SolarWindsApmConfig() + def test_to_configuration_default(apm): config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert isinstance(config, Configuration) @@ -694,41 +840,50 @@ def test_to_configuration_default(apm): assert config.transaction_name == apm.get("transaction_name") assert isinstance(config.transaction_settings, list) + def test_to_configuration_with_service_key(apm): apm._set_config_value("service_key", "test_token:test_service") config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert config.headers["Authorization"] == "Bearer test_token" + def test_to_configuration_with_transaction_filters(apm): - apm._set_config_value("transaction_filters", [ - {"tracing_mode": 1, "regex": re.compile(".*")} - ]) + apm._set_config_value( + "transaction_filters", [{"tracing_mode": 1, "regex": re.compile(".*")}] + ) config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert len(config.transaction_settings) == 1 assert isinstance(config.transaction_settings[0], TransactionSetting) assert config.transaction_settings[0].tracing is True + def test_to_configuration_with_disabled_tracing(apm): apm._set_config_value("tracing_mode", "disabled") config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert config.tracing_mode is False + def test_to_configuration_with_disabled_trigger_trace(apm): apm._set_config_value("trigger_trace", "disabled") config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert config.trigger_trace_enabled is False + def test_to_configuration_with_empty_transaction_filters(apm): apm._set_config_value("transaction_filters", []) config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert isinstance(config.transaction_settings, list) assert len(config.transaction_settings) == 0 + def test_to_configuration_with_multiple_transaction_filters(apm): - apm._set_config_value("transaction_filters", [ - {"tracing_mode": 1, "regex": re.compile(".*")}, - {"tracing_mode": 0, "regex": re.compile("foo")} - ]) + apm._set_config_value( + "transaction_filters", + [ + {"tracing_mode": 1, "regex": re.compile(".*")}, + {"tracing_mode": 0, "regex": re.compile("foo")}, + ], + ) config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert len(config.transaction_settings) == 2 assert isinstance(config.transaction_settings[0], TransactionSetting) @@ -736,32 +891,40 @@ def test_to_configuration_with_multiple_transaction_filters(apm): assert isinstance(config.transaction_settings[1], TransactionSetting) assert config.transaction_settings[1].tracing is False + def test_to_configuration_with_invalid_service_key(apm): apm._set_config_value("service_key", "invalid_format") config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert config.headers["Authorization"] == "Bearer invalid_format" + def test_to_configuration_with_empty_service_key(apm): apm._set_config_value("service_key", "") config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert config.headers["Authorization"] == "Bearer " + def test_to_configuration_with_disabled_agent(apm): apm.agent_enabled = False config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert config.enabled is False + def test_to_configuration_with_enabled_agent(apm): apm.agent_enabled = True config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) assert config.enabled is True + def test_to_configuration_attribute_error_non_string_service_key( mocker, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "token:service", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "token:service", + }, + ) test_apm_config = apm_config.SolarWindsApmConfig() test_apm_config._SolarWindsApmConfig__config["service_key"] = None config = apm_config.SolarWindsApmConfig.to_configuration(test_apm_config) diff --git a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py index d6e4949a2..1fa18dfc5 100644 --- a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py +++ b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py @@ -6,19 +6,14 @@ import logging import os + import pytest from solarwinds_apm import apm_config # pylint: disable=unused-import -from .fixtures.cnf_dict import ( - fixture_cnf_dict, - fixture_cnf_dict_enabled_false, - fixture_cnf_dict_enabled_false_mixed_case, -) # pylint: disable=unused-import -from .fixtures.env_vars import fixture_mock_env_vars @pytest.fixture @@ -26,22 +21,26 @@ def setup_caplog(): apm_logger = logging.getLogger("solarwinds_apm") apm_logger.propagate = True + class TestSolarWindsApmConfigAgentEnabled: def test_calculate_agent_enabled_service_key_missing(self, mocker): # Save any service key in os for later old_service_key = os.environ.get("SW_APM_SERVICE_KEY", None) if old_service_key: del os.environ["SW_APM_SERVICE_KEY"] - mocker.patch.dict(os.environ, { - "SW_APM_AGENT_ENABLED": "true", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_AGENT_ENABLED": "true", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) @@ -52,11 +51,8 @@ def test_calculate_agent_enabled_service_key_missing(self, mocker): if old_service_key: os.environ["SW_APM_SERVICE_KEY"] = old_service_key - def test_calculate_agent_enabled_service_key_env_var_set_cnf_file_ignored( - self, - mocker, - fixture_cnf_dict, - mock_env_vars + def test_calculate_agent_enabled_service_key_env_var_set_cnf_file_ignored( + self, mocker, fixture_cnf_dict, mock_env_vars ): # Save any service key in os for later old_service_key = os.environ.get("SW_APM_SERVICE_KEY", None) @@ -65,23 +61,24 @@ def test_calculate_agent_enabled_service_key_env_var_set_cnf_file_ignored( if old_collector: del os.environ["SW_APM_COLLECTOR"] - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key-will-be-used", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key-will-be-used", + }, + ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) # Includes "serviceKey": "not-good-to-put-here:still-could-be-used" - mock_get_cnf_dict.configure_mock( - return_value=fixture_cnf_dict - ) + mock_get_cnf_dict.configure_mock(return_value=fixture_cnf_dict) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) @@ -97,7 +94,7 @@ def test_calculate_agent_enabled_service_key_env_var_set_cnf_file_ignored( if old_service_key: os.environ["SW_APM_SERVICE_KEY"] = old_service_key - def test_calculate_agent_enabled_service_key_env_var_not_set_cnf_file_used( + def test_calculate_agent_enabled_service_key_env_var_not_set_cnf_file_used( self, mocker, fixture_cnf_dict, @@ -116,21 +113,22 @@ def test_calculate_agent_enabled_service_key_env_var_not_set_cnf_file_used( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) # Includes "serviceKey": "not-good-to-put-here:still-could-be-used" - mock_get_cnf_dict.configure_mock( - return_value=fixture_cnf_dict - ) + mock_get_cnf_dict.configure_mock(return_value=fixture_cnf_dict) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() - assert resulting_config.get("service_key") == "not-good-to-put-here:still-could-be-used" + assert ( + resulting_config.get("service_key") + == "not-good-to-put-here:still-could-be-used" + ) assert resulting_config.agent_enabled assert resulting_config.service_name == "still-could-be-used" @@ -142,17 +140,20 @@ def test_calculate_agent_enabled_service_key_env_var_not_set_cnf_file_used( os.environ["SW_APM_SERVICE_KEY"] = old_service_key def test_calculate_agent_enabled_service_key_bad_format(self, mocker): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "invalidkey", - "SW_APM_AGENT_ENABLED": "true", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "invalidkey", + "SW_APM_AGENT_ENABLED": "true", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -164,16 +165,19 @@ def test_calculate_agent_enabled_service_key_ok( mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -185,17 +189,20 @@ def test_calculate_agent_enabled_env_var_true( mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - "SW_APM_AGENT_ENABLED": "true", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + "SW_APM_AGENT_ENABLED": "true", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -203,17 +210,20 @@ def test_calculate_agent_enabled_env_var_true( assert resulting_config.service_name == "key" def test_calculate_agent_enabled_env_var_false(self, mocker): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - "SW_APM_AGENT_ENABLED": "false", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + "SW_APM_AGENT_ENABLED": "false", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -221,17 +231,20 @@ def test_calculate_agent_enabled_env_var_false(self, mocker): assert resulting_config.service_name == "" def test_calculate_agent_enabled_env_var_false_mixed_case(self, mocker): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - "SW_APM_AGENT_ENABLED": "fALsE", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + "SW_APM_AGENT_ENABLED": "fALsE", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -243,9 +256,12 @@ def test_calculate_agent_enabled_env_var_not_set_cnf_file_false( mocker, fixture_cnf_dict_enabled_false, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) @@ -259,7 +275,7 @@ def test_calculate_agent_enabled_env_var_not_set_cnf_file_false( mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -271,9 +287,12 @@ def test_calculate_agent_enabled_env_var_not_set_cnf_file_false_mixed_case( mocker, fixture_cnf_dict_enabled_false_mixed_case, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) @@ -287,7 +306,7 @@ def test_calculate_agent_enabled_env_var_not_set_cnf_file_false_mixed_case( mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -300,10 +319,13 @@ def test_calculate_agent_enabled_env_var_true_cnf_file_false( fixture_cnf_dict_enabled_false, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - "SW_APM_AGENT_ENABLED": "true", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + "SW_APM_AGENT_ENABLED": "true", + }, + ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) @@ -316,7 +338,7 @@ def test_calculate_agent_enabled_env_var_true_cnf_file_false( mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -328,23 +350,24 @@ def test_calculate_agent_enabled_env_var_false_cnf_file_true( mocker, fixture_cnf_dict, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key", - "SW_APM_AGENT_ENABLED": "false", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key", + "SW_APM_AGENT_ENABLED": "false", + }, + ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) - mock_get_cnf_dict.configure_mock( - return_value=fixture_cnf_dict - ) + mock_get_cnf_dict.configure_mock(return_value=fixture_cnf_dict) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) @@ -358,18 +381,21 @@ def test_calculate_agent_enabled_ok_all_env_vars( mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "OTEL_PROPAGATORS": "foo,solarwinds_propagator,bar", - "SW_APM_SERVICE_KEY": "valid:key", - "SW_APM_AGENT_ENABLED": "true", - }) + mocker.patch.dict( + os.environ, + { + "OTEL_PROPAGATORS": "foo,solarwinds_propagator,bar", + "SW_APM_SERVICE_KEY": "valid:key", + "SW_APM_AGENT_ENABLED": "true", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -377,17 +403,20 @@ def test_calculate_agent_enabled_ok_all_env_vars( assert resulting_config.service_name == "key" def test_calculate_agent_enabled_no_sw_propagator(self, mocker): - mocker.patch.dict(os.environ, { - "OTEL_PROPAGATORS": "tracecontext,baggage", - "SW_APM_SERVICE_KEY": "valid:key", - }) + mocker.patch.dict( + os.environ, + { + "OTEL_PROPAGATORS": "tracecontext,baggage", + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -395,54 +424,68 @@ def test_calculate_agent_enabled_no_sw_propagator(self, mocker): assert resulting_config.service_name == "" def test_calculate_agent_enabled_no_tracecontext_propagator(self, mocker): - mocker.patch.dict(os.environ, { - "OTEL_PROPAGATORS": "solarwinds_propagator", - "SW_APM_SERVICE_KEY": "valid:key", - }) + mocker.patch.dict( + os.environ, + { + "OTEL_PROPAGATORS": "solarwinds_propagator", + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() assert resulting_config._calculate_agent_enabled() assert resulting_config.service_name == "key" - def test_calculate_agent_enabled_sw_with_tracecontext_propagator(self, caplog, mocker): - mocker.patch.dict(os.environ, { - "OTEL_PROPAGATORS": "solarwinds_propagator,tracecontext", - "SW_APM_SERVICE_KEY": "valid:key", - }) + def test_calculate_agent_enabled_sw_with_tracecontext_propagator( + self, caplog, mocker + ): + mocker.patch.dict( + os.environ, + { + "OTEL_PROPAGATORS": "solarwinds_propagator,tracecontext", + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() assert not resulting_config._calculate_agent_enabled() assert resulting_config.service_name == "" - assert "It is unnecessary to configure tracecontext in OTEL_PROPAGATORS when using SolarWinds APM >= 4.4.0, which has built-in w3c context propagation" in caplog.text + assert ( + "It is unnecessary to configure tracecontext in OTEL_PROPAGATORS when using SolarWinds APM >= 4.4.0, which has built-in w3c context propagation" + in caplog.text + ) def test_calculate_agent_enabled_sw_and_baggage_propagator(self, mocker): - mocker.patch.dict(os.environ, { - "OTEL_PROPAGATORS": "solarwinds_propagator,baggage", - "SW_APM_SERVICE_KEY": "valid:key", - }) + mocker.patch.dict( + os.environ, + { + "OTEL_PROPAGATORS": "solarwinds_propagator,baggage", + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -450,19 +493,22 @@ def test_calculate_agent_enabled_sw_and_baggage_propagator(self, mocker): assert resulting_config.service_name == "key" def test_calculate_agent_enabled_baggage_and_sw_propagator(self, mocker): - mocker.patch.dict(os.environ, { - "OTEL_PROPAGATORS": "baggage,solarwinds_propagator", - "SW_APM_SERVICE_KEY": "valid:key", - }) + mocker.patch.dict( + os.environ, + { + "OTEL_PROPAGATORS": "baggage,solarwinds_propagator", + "SW_APM_SERVICE_KEY": "valid:key", + }, + ) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() assert resulting_config._calculate_agent_enabled() - assert resulting_config.service_name == "key" \ No newline at end of file + assert resulting_config.service_name == "key" diff --git a/tests/unit/test_apm_config/test_apm_config_calculate_collector.py b/tests/unit/test_apm_config/test_apm_config_calculate_collector.py index 810afb2c2..c0bfb83f8 100644 --- a/tests/unit/test_apm_config/test_apm_config_calculate_collector.py +++ b/tests/unit/test_apm_config/test_apm_config_calculate_collector.py @@ -5,9 +5,12 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. import os + import pytest + from solarwinds_apm.apm_config import SolarWindsApmConfig + class TestSolarWindsApmConfigCalculateCollector: @pytest.fixture(autouse=True) def clear_env_vars(self): @@ -15,29 +18,61 @@ def clear_env_vars(self): os.environ.clear() def test_calculate_collector_default(self, mocker): - assert SolarWindsApmConfig.calculate_collector() == "apm.collector.na-01.cloud.solarwinds.com" + assert ( + SolarWindsApmConfig.calculate_collector() + == "apm.collector.na-01.cloud.solarwinds.com" + ) def test_calculate_collector_with_env_var_set(self, mocker): mocker.patch.dict(os.environ, {"SW_APM_COLLECTOR": "foo-collector"}) assert SolarWindsApmConfig.calculate_collector() == "foo-collector" def test_calculate_collector_with_config_not_provided(self, mocker): - mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict', return_value={"collector": "bar-collector"}) + mocker.patch.object( + SolarWindsApmConfig, + "get_cnf_dict", + return_value={"collector": "bar-collector"}, + ) assert SolarWindsApmConfig.calculate_collector() == "bar-collector" - def test_calculate_collector_with_config_not_provided_and_env_var(self, mocker): + def test_calculate_collector_with_config_not_provided_and_env_var( + self, mocker + ): mocker.patch.dict(os.environ, {"SW_APM_COLLECTOR": "foo-collector"}) - mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict', return_value={"collector": "bar-collector"}) + mocker.patch.object( + SolarWindsApmConfig, + "get_cnf_dict", + return_value={"collector": "bar-collector"}, + ) assert SolarWindsApmConfig.calculate_collector() == "foo-collector" def test_calculate_collector_with_provided_cnf_dict(self, mocker): - mock_get_cnf_dict = mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict', return_value={"collector": "bar-collector"}) - assert SolarWindsApmConfig.calculate_collector(cnf_dict={"collector": "baz-collector"}) == "baz-collector" + mock_get_cnf_dict = mocker.patch.object( + SolarWindsApmConfig, + "get_cnf_dict", + return_value={"collector": "bar-collector"}, + ) + assert ( + SolarWindsApmConfig.calculate_collector( + cnf_dict={"collector": "baz-collector"} + ) + == "baz-collector" + ) mock_get_cnf_dict.assert_not_called() - def test_calculate_collector_with_provided_cnf_dict_while_env(self, mocker): + def test_calculate_collector_with_provided_cnf_dict_while_env( + self, mocker + ): mocker.patch.dict(os.environ, {"SW_APM_COLLECTOR": "foo-collector"}) - mock_get_cnf_dict = mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict', return_value={"collector": "bar-collector"}) - assert SolarWindsApmConfig.calculate_collector(cnf_dict={"collector": "baz-collector"}) == "foo-collector" + mock_get_cnf_dict = mocker.patch.object( + SolarWindsApmConfig, + "get_cnf_dict", + return_value={"collector": "bar-collector"}, + ) + assert ( + SolarWindsApmConfig.calculate_collector( + cnf_dict={"collector": "baz-collector"} + ) + == "foo-collector" + ) mock_get_cnf_dict.assert_not_called() - diff --git a/tests/unit/test_apm_config/test_apm_config_calculate_metrics_enabled.py b/tests/unit/test_apm_config/test_apm_config_calculate_metrics_enabled.py index fa487ff90..7749e9ac3 100644 --- a/tests/unit/test_apm_config/test_apm_config_calculate_metrics_enabled.py +++ b/tests/unit/test_apm_config/test_apm_config_calculate_metrics_enabled.py @@ -5,9 +5,12 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. import os + import pytest + from solarwinds_apm.apm_config import SolarWindsApmConfig + class TestSolarWindsApmConfigCalculateMetricsEnabled: @pytest.fixture(autouse=True) def clear_env_vars(self): @@ -15,34 +18,72 @@ def clear_env_vars(self): os.environ.clear() def test_calculate_metrics_enabled_with_env_var_true(self, mocker): - mocker.patch.dict(os.environ, {"SW_APM_EXPORT_METRICS_ENABLED": "true"}) + mocker.patch.dict( + os.environ, {"SW_APM_EXPORT_METRICS_ENABLED": "true"} + ) assert SolarWindsApmConfig.calculate_metrics_enabled() is True def test_calculate_metrics_enabled_with_env_var_false(self, mocker): - mocker.patch.dict(os.environ, {"SW_APM_EXPORT_METRICS_ENABLED": "false"}) + mocker.patch.dict( + os.environ, {"SW_APM_EXPORT_METRICS_ENABLED": "false"} + ) assert SolarWindsApmConfig.calculate_metrics_enabled() is False - def test_calculate_metrics_enabled_with_config_not_provided_true(self, mocker): - mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict', return_value={"export_metrics_enabled": "true"}) - mocker.patch.object(SolarWindsApmConfig, 'convert_to_bool', return_value=True) + def test_calculate_metrics_enabled_with_config_not_provided_true( + self, mocker + ): + mocker.patch.object( + SolarWindsApmConfig, + "get_cnf_dict", + return_value={"export_metrics_enabled": "true"}, + ) + mocker.patch.object( + SolarWindsApmConfig, "convert_to_bool", return_value=True + ) assert SolarWindsApmConfig.calculate_metrics_enabled() is True - def test_calculate_metrics_enabled_with_config_not_provided_false(self, mocker): - mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict', return_value={"export_metrics_enabled": "false"}) - mocker.patch.object(SolarWindsApmConfig, 'convert_to_bool', return_value=False) + def test_calculate_metrics_enabled_with_config_not_provided_false( + self, mocker + ): + mocker.patch.object( + SolarWindsApmConfig, + "get_cnf_dict", + return_value={"export_metrics_enabled": "false"}, + ) + mocker.patch.object( + SolarWindsApmConfig, "convert_to_bool", return_value=False + ) assert SolarWindsApmConfig.calculate_metrics_enabled() is False def test_calculate_metrics_enabled_with_config_not_boolean(self, mocker): - assert SolarWindsApmConfig.calculate_metrics_enabled( - cnf_dict={"export_metrics_enabled": "foo-bar"} - ) is True + assert ( + SolarWindsApmConfig.calculate_metrics_enabled( + cnf_dict={"export_metrics_enabled": "foo-bar"} + ) + is True + ) - def test_calculate_metrics_enabled_with_config_not_provided_and_env_var(self, mocker): - mocker.patch.dict(os.environ, {"SW_APM_EXPORT_METRICS_ENABLED": "true"}) - mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict', return_value={"export_metrics_enabled": "false"}) + def test_calculate_metrics_enabled_with_config_not_provided_and_env_var( + self, mocker + ): + mocker.patch.dict( + os.environ, {"SW_APM_EXPORT_METRICS_ENABLED": "true"} + ) + mocker.patch.object( + SolarWindsApmConfig, + "get_cnf_dict", + return_value={"export_metrics_enabled": "false"}, + ) assert SolarWindsApmConfig.calculate_metrics_enabled() is True def test_calculate_metrics_enabled_with_provided_cnf_dict(self, mocker): - mock_get_cnf_dict = mocker.patch.object(SolarWindsApmConfig, 'get_cnf_dict') - assert SolarWindsApmConfig.calculate_metrics_enabled(cnf_dict={"export_metrics_enabled": True}) is True + mock_get_cnf_dict = mocker.patch.object( + SolarWindsApmConfig, "get_cnf_dict" + ) + assert ( + SolarWindsApmConfig.calculate_metrics_enabled( + cnf_dict={"export_metrics_enabled": True} + ) + is True + ) mock_get_cnf_dict.assert_not_called() diff --git a/tests/unit/test_apm_config/test_apm_config_cnf_file.py b/tests/unit/test_apm_config/test_apm_config_cnf_file.py index ee8b9f44a..70fdeb1ec 100644 --- a/tests/unit/test_apm_config/test_apm_config_cnf_file.py +++ b/tests/unit/test_apm_config/test_apm_config_cnf_file.py @@ -10,24 +10,21 @@ from solarwinds_apm import apm_config # pylint: disable=unused-import -from .fixtures.cnf_dict import fixture_cnf_dict + # pylint: disable=unused-import -from .fixtures.cnf_file import ( - fixture_cnf_file, - fixture_cnf_file_invalid_json, -) + # pylint: disable=unused-import -from .fixtures.env_vars import fixture_mock_env_vars + class TestSolarWindsApmConfigCnfFile: # pylint:disable=unused-argument def test_get_cnf_dict_default_path_no_file( - self, - mock_env_vars, - ): + self, + mock_env_vars, + ): # use key from env var, agent enabled, nothing has errored resulting_config = apm_config.SolarWindsApmConfig() - assert resulting_config.agent_enabled == True + assert resulting_config.agent_enabled assert resulting_config.get("service_key") == "valid:key" # cnf_dict is none assert resulting_config.get_cnf_dict() is None @@ -37,13 +34,16 @@ def test_get_cnf_dict_custom_path_no_file( mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key-service-name", - "SW_APM_CONFIG_FILE": "nothing-is-here", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key-service-name", + "SW_APM_CONFIG_FILE": "nothing-is-here", + }, + ) # use key from env var, agent enabled, nothing has errored resulting_config = apm_config.SolarWindsApmConfig() - assert resulting_config.agent_enabled == True + assert resulting_config.agent_enabled assert resulting_config.get("service_key") == "valid:key-service-name" # cnf_dict is none assert resulting_config.get_cnf_dict() is None @@ -55,13 +55,16 @@ def test_get_cnf_dict_not_valid_json( fixture_cnf_file_invalid_json, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key-service-name", - "SW_APM_CONFIG_FILE": "nothing-is-here", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key-service-name", + "SW_APM_CONFIG_FILE": "nothing-is-here", + }, + ) # use key from env var, agent enabled, nothing has errored resulting_config = apm_config.SolarWindsApmConfig() - assert resulting_config.agent_enabled == True + assert resulting_config.agent_enabled assert resulting_config.get("service_key") == "valid:key-service-name" # cnf_dict is none assert resulting_config.get_cnf_dict() is None @@ -73,13 +76,16 @@ def test_get_cnf_dict( fixture_cnf_file, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key-service-name", - "SW_APM_CONFIG_FILE": "nothing-is-here", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key-service-name", + "SW_APM_CONFIG_FILE": "nothing-is-here", + }, + ) # use key from env var, agent enabled, nothing has errored resulting_config = apm_config.SolarWindsApmConfig() - assert resulting_config.agent_enabled == True + assert resulting_config.agent_enabled assert resulting_config.get("service_key") == "valid:key-service-name" # cnf_dict is dict with kv from fixture assert resulting_config.get_cnf_dict() == {"foo": "bar"} @@ -96,36 +102,37 @@ def test_update_with_cnf_file_all_valid( if old_collector: del os.environ["SW_APM_COLLECTOR"] - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key-service-name", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key-service-name", + }, + ) mock_update_txn_filters = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.update_transaction_filters" ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) - mock_get_cnf_dict.configure_mock( - return_value=fixture_cnf_dict - ) + mock_get_cnf_dict.configure_mock(return_value=fixture_cnf_dict) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) # use key from env var (Python APM only uses key from here), # agent enabled, nothing has errored resulting_config = apm_config.SolarWindsApmConfig() - assert resulting_config.agent_enabled == True + assert resulting_config.agent_enabled assert resulting_config.get("service_key") == "valid:key-service-name" # config includes snake_case versions of mock's camelCase keys # and valid values - assert resulting_config.agent_enabled == True + assert resulting_config.agent_enabled assert resulting_config.get("tracing_mode") == 1 assert resulting_config.get("trigger_trace") == 1 assert resulting_config.get("collector") == "foo-bar" @@ -147,9 +154,12 @@ def test_update_with_cnf_file_mostly_invalid( if old_collector: del os.environ["SW_APM_COLLECTOR"] - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key-service-name", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key-service-name", + }, + ) mock_update_txn_filters = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.update_transaction_filters" ) @@ -167,22 +177,20 @@ def test_update_with_cnf_file_mostly_invalid( mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) - mock_get_cnf_dict.configure_mock( - return_value=mostly_invalid_cnf_dict - ) + mock_get_cnf_dict.configure_mock(return_value=mostly_invalid_cnf_dict) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) # use key from env var (Python APM only uses key from here), # agent enabled, nothing has errored resulting_config = apm_config.SolarWindsApmConfig() - assert resulting_config.agent_enabled == True + assert resulting_config.agent_enabled assert resulting_config.get("service_key") == "valid:key-service-name" # config includes snake_case versions of mock's camelCase keys # and default values because invalid ones ignored @@ -193,7 +201,9 @@ def test_update_with_cnf_file_mostly_invalid( assert resulting_config.get("collector") == "False" # update_transaction_filters was called - mock_update_txn_filters.assert_called_once_with(mostly_invalid_cnf_dict) + mock_update_txn_filters.assert_called_once_with( + mostly_invalid_cnf_dict + ) # Restore old collector if old_collector: os.environ["SW_APM_COLLECTOR"] = old_collector @@ -209,30 +219,31 @@ def test_update_with_cnf_file_and_all_validenv_vars( if old_collector: del os.environ["SW_APM_COLLECTOR"] - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "valid:key-service-name", - "SW_APM_AGENT_ENABLED": "false", - "SW_APM_TRACING_MODE": "disabled", - "SW_APM_TRIGGER_TRACE": "disabled", - "SW_APM_COLLECTOR": "other-foo-bar", - "SW_APM_DEBUG_LEVEL": "5", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "valid:key-service-name", + "SW_APM_AGENT_ENABLED": "false", + "SW_APM_TRACING_MODE": "disabled", + "SW_APM_TRIGGER_TRACE": "disabled", + "SW_APM_COLLECTOR": "other-foo-bar", + "SW_APM_DEBUG_LEVEL": "5", + }, + ) mock_update_txn_filters = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.update_transaction_filters" ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) - mock_get_cnf_dict.configure_mock( - return_value=fixture_cnf_dict - ) + mock_get_cnf_dict.configure_mock(return_value=fixture_cnf_dict) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -243,7 +254,7 @@ def test_update_with_cnf_file_and_all_validenv_vars( assert resulting_config.get("service_key") == "valid:key-service-name" # Rest of config prioritizes env_var > cnf_file - assert resulting_config.agent_enabled == False + assert not resulting_config.agent_enabled assert resulting_config.get("tracing_mode") == 0 assert resulting_config.get("trigger_trace") == 0 assert resulting_config.get("collector") == "other-foo-bar" @@ -264,30 +275,31 @@ def test_update_with_cnf_file_and_several_invalid_env_vars( if old_collector: del os.environ["SW_APM_COLLECTOR"] - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "not-valid-and-agent-will-be-disabled", - "SW_APM_AGENT_ENABLED": "other-foo-bar", - "SW_APM_TRACING_MODE": "other-foo-bar", - "SW_APM_TRIGGER_TRACE": "other-foo-bar", - "SW_APM_COLLECTOR": "False", - "SW_APM_DEBUG_LEVEL": "other-foo-bar", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "not-valid-and-agent-will-be-disabled", + "SW_APM_AGENT_ENABLED": "other-foo-bar", + "SW_APM_TRACING_MODE": "other-foo-bar", + "SW_APM_TRIGGER_TRACE": "other-foo-bar", + "SW_APM_COLLECTOR": "False", + "SW_APM_DEBUG_LEVEL": "other-foo-bar", + }, + ) mock_update_txn_filters = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.update_transaction_filters" ) mock_get_cnf_dict = mocker.patch( "solarwinds_apm.apm_config.SolarWindsApmConfig.get_cnf_dict" ) - mock_get_cnf_dict.configure_mock( - return_value=fixture_cnf_dict - ) + mock_get_cnf_dict.configure_mock(return_value=fixture_cnf_dict) mock_apm_logging = mocker.patch( "solarwinds_apm.apm_config.apm_logging" ) mock_apm_logging.configure_mock( **{ "set_sw_log_level": mocker.Mock(), - "ApmLoggingLevel.default_level": mocker.Mock(return_value=2) + "ApmLoggingLevel.default_level": mocker.Mock(return_value=2), } ) resulting_config = apm_config.SolarWindsApmConfig() @@ -296,8 +308,11 @@ def test_update_with_cnf_file_and_several_invalid_env_vars( # even if invalid, only service_key from env var used # and APM will be disabled - assert resulting_config.agent_enabled == False - assert resulting_config.get("service_key") == "not-valid-and-agent-will-be-disabled" # the full key does not print to std out and appears masked + assert not resulting_config.agent_enabled + assert ( + resulting_config.get("service_key") + == "not-valid-and-agent-will-be-disabled" + ) # the full key does not print to std out and appears masked # cnf_file values from fixture_cnf_dict are kept if same env_var invalid assert resulting_config.get("tracing_mode") == 1 diff --git a/tests/unit/test_apm_config/test_apm_config_service_name.py b/tests/unit/test_apm_config/test_apm_config_service_name.py index ffb183de2..b99b6eceb 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_name.py +++ b/tests/unit/test_apm_config/test_apm_config_service_name.py @@ -11,7 +11,7 @@ from solarwinds_apm import apm_config # pylint: disable=unused-import -from .fixtures.env_vars import fixture_mock_env_vars + class TestSolarWindsApmConfigServiceName: def test__calculate_service_name_is_lambda(self, mocker): @@ -80,10 +80,7 @@ def test__calculate_service_name_not_is_lambda(self, mocker): class TestSolarWindsApmConfigServiceNameApmProto: def test__calculate_service_name_apm_proto_agent_disabled(self): test_config = apm_config.SolarWindsApmConfig() - result = test_config._calculate_service_name_apm_proto( - False, - {} - ) + result = test_config._calculate_service_name_apm_proto(False, {}) assert result == "" def test__calculate_service_name_apm_proto_no_otel_service_name( @@ -91,13 +88,15 @@ def test__calculate_service_name_apm_proto_no_otel_service_name( mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + }, + ) test_config = apm_config.SolarWindsApmConfig() result = test_config._calculate_service_name_apm_proto( - True, - Resource.create({"service.name": None}) + True, Resource.create({"service.name": None}) ) assert result == "sw_service_name" @@ -106,14 +105,17 @@ def test__calculate_service_name_apm_proto_default_unknown_otel_service_name( mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + }, + ) test_config = apm_config.SolarWindsApmConfig() result = test_config._calculate_service_name_apm_proto( True, # default is unknown_service - Resource.create() + Resource.create(), ) assert result == "sw_service_name" @@ -122,13 +124,15 @@ def test__calculate_service_name_apm_proto_use_otel_service_name( mocker, mock_env_vars, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "service_key_with:sw_service_name", + }, + ) test_config = apm_config.SolarWindsApmConfig() result = test_config._calculate_service_name_apm_proto( - True, - Resource.create({"service.name": "foobar"}) + True, Resource.create({"service.name": "foobar"}) ) assert result == "foobar" @@ -136,13 +140,16 @@ def test__calculate_service_name_apm_proto_malformed_service_key_only_token( self, mocker, ): - mocker.patch.dict(os.environ, { - "SW_APM_SERVICE_KEY": "token:", - }) + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "token:", + }, + ) test_config = apm_config.SolarWindsApmConfig() result = test_config._calculate_service_name_apm_proto( True, - Resource.create() # default is unknown_service + Resource.create(), # default is unknown_service ) assert result == "" @@ -154,18 +161,22 @@ def test__calculate_service_name_apm_proto_non_string_service_key( test_config._SolarWindsApmConfig__config["service_key"] = 123 result = test_config._calculate_service_name_apm_proto( True, - Resource.create() # default is unknown_service + Resource.create(), # default is unknown_service ) assert result == "" + class TestSolarWindsApmConfigServiceNameLambda: def test__calculate_service_name_lambda_no_otel_name( self, mocker, ): - mocker.patch.dict(os.environ, { - "AWS_LAMBDA_FUNCTION_NAME": "foo-fn", - }) + mocker.patch.dict( + os.environ, + { + "AWS_LAMBDA_FUNCTION_NAME": "foo-fn", + }, + ) test_config = apm_config.SolarWindsApmConfig() result = test_config._calculate_service_name_lambda( Resource.create({}) @@ -176,9 +187,12 @@ def test__calculate_service_name_lambda_empty_otel_name( self, mocker, ): - mocker.patch.dict(os.environ, { - "AWS_LAMBDA_FUNCTION_NAME": "foo-fn", - }) + mocker.patch.dict( + os.environ, + { + "AWS_LAMBDA_FUNCTION_NAME": "foo-fn", + }, + ) test_config = apm_config.SolarWindsApmConfig() result = test_config._calculate_service_name_lambda( Resource.create({"service.name": ""}) @@ -189,9 +203,12 @@ def test__calculate_service_name_lambda_otel_name_unknown( self, mocker, ): - mocker.patch.dict(os.environ, { - "AWS_LAMBDA_FUNCTION_NAME": "foo-fn", - }) + mocker.patch.dict( + os.environ, + { + "AWS_LAMBDA_FUNCTION_NAME": "foo-fn", + }, + ) test_config = apm_config.SolarWindsApmConfig() result = test_config._calculate_service_name_lambda( Resource.create({"service.name": "unknown_service"}) diff --git a/tests/unit/test_apm_config/test_apm_config_transaction_filters.py b/tests/unit/test_apm_config/test_apm_config_transaction_filters.py index d2d19fa0a..a62bd07fa 100644 --- a/tests/unit/test_apm_config/test_apm_config_transaction_filters.py +++ b/tests/unit/test_apm_config/test_apm_config_transaction_filters.py @@ -8,6 +8,7 @@ from solarwinds_apm import apm_config + class TestSolarWindsApmConfigTxnFilters: def test_update_transaction_filters_none(self): cfg = apm_config.SolarWindsApmConfig() @@ -16,92 +17,48 @@ def test_update_transaction_filters_none(self): def test_update_transaction_filters_not_list(self): cfg = apm_config.SolarWindsApmConfig() - cfg.update_transaction_filters( - { - "transactionSettings": "foo" - } - ) + cfg.update_transaction_filters({"transactionSettings": "foo"}) assert cfg.get("transaction_filters") == [] def test_update_transaction_filters_missing_tracing(self): cfg = apm_config.SolarWindsApmConfig() cfg.update_transaction_filters( - { - "transactionSettings": [ - { - "regex": "foo" - } - ] - } + {"transactionSettings": [{"regex": "foo"}]} ) assert cfg.get("transaction_filters") == [] def test_update_transaction_filters_invalid_tracing(self): cfg = apm_config.SolarWindsApmConfig() cfg.update_transaction_filters( - { - "transactionSettings": [ - { - "regex": "foo", - "tracing": "not-valid" - } - ] - } + {"transactionSettings": [{"regex": "foo", "tracing": "not-valid"}]} ) assert cfg.get("transaction_filters") == [] def test_update_transaction_filters_missing_regex(self): cfg = apm_config.SolarWindsApmConfig() cfg.update_transaction_filters( - { - "transactionSettings": [ - { - "tracing": "enabled" - } - ] - } + {"transactionSettings": [{"tracing": "enabled"}]} ) assert cfg.get("transaction_filters") == [] def test_update_transaction_filters_invalid_type_regex(self): cfg = apm_config.SolarWindsApmConfig() cfg.update_transaction_filters( - { - "transactionSettings": [ - { - "regex": 123, - "tracing": "enabled" - } - ] - } + {"transactionSettings": [{"regex": 123, "tracing": "enabled"}]} ) assert cfg.get("transaction_filters") == [] def test_update_transaction_filters_empty_regex(self): cfg = apm_config.SolarWindsApmConfig() cfg.update_transaction_filters( - { - "transactionSettings": [ - { - "regex": "", - "tracing": "enabled" - } - ] - } + {"transactionSettings": [{"regex": "", "tracing": "enabled"}]} ) assert cfg.get("transaction_filters") == [] def test_update_transaction_filters_invalid_compile_regex(self): cfg = apm_config.SolarWindsApmConfig() cfg.update_transaction_filters( - { - "transactionSettings": [ - { - "regex": "[", - "tracing": "enabled" - } - ] - } + {"transactionSettings": [{"regex": "[", "tracing": "enabled"}]} ) assert cfg.get("transaction_filters") == [] @@ -110,26 +67,14 @@ def test_update_transaction_filters(self): cfg.update_transaction_filters( { "transactionSettings": [ - { - "regex": "foo", - "tracing": "enabled" - }, - { - "regex": "bar", - "tracing": "disabled" - } + {"regex": "foo", "tracing": "enabled"}, + {"regex": "bar", "tracing": "disabled"}, ] } ) assert cfg.get("transaction_filters") == [ - { - "regex": re.compile("foo"), - "tracing_mode": 1 - }, - { - "regex": re.compile("bar"), - "tracing_mode": 0 - } + {"regex": re.compile("foo"), "tracing_mode": 1}, + {"regex": re.compile("bar"), "tracing_mode": 0}, ] def test_update_transaction_filters_multiple_regex_use_first(self): @@ -137,32 +82,14 @@ def test_update_transaction_filters_multiple_regex_use_first(self): cfg.update_transaction_filters( { "transactionSettings": [ - { - "regex": "foo", - "tracing": "enabled" - }, - { - "regex": "bar", - "tracing": "disabled" - }, - { - "regex": "foo", - "tracing": "disabled" - }, - { - "regex": "bar", - "tracing": "enabled" - } + {"regex": "foo", "tracing": "enabled"}, + {"regex": "bar", "tracing": "disabled"}, + {"regex": "foo", "tracing": "disabled"}, + {"regex": "bar", "tracing": "enabled"}, ] } ) assert cfg.get("transaction_filters") == [ - { - "regex": re.compile("foo"), - "tracing_mode": 1 - }, - { - "regex": re.compile("bar"), - "tracing_mode": 0 - } + {"regex": re.compile("foo"), "tracing_mode": 1}, + {"regex": re.compile("bar"), "tracing_mode": 0}, ] diff --git a/tests/unit/test_apm_logging.py b/tests/unit/test_apm_logging.py index 4f32cdb2b..0dae56d30 100644 --- a/tests/unit/test_apm_logging.py +++ b/tests/unit/test_apm_logging.py @@ -6,6 +6,7 @@ from solarwinds_apm import apm_logging + class TestApmLoggingLevel: def test_default_level(self): assert apm_logging.ApmLoggingLevel.default_level() == 2 @@ -36,25 +37,21 @@ def get_mock_logger_and_rfhandler( mocker, error=False, ): - mock_apm_logger = mocker.patch( - "solarwinds_apm.apm_logging.logger" - ) + mock_apm_logger = mocker.patch("solarwinds_apm.apm_logging.logger") mock_warning = mocker.Mock() mock_error = mocker.Mock() mock_addhandler = mocker.Mock() mock_rmhandler = mocker.Mock() mock_apm_logger.configure_mock( - **{ - "addHandler": mock_addhandler, - "error": mock_error, - "removeHandler": mock_rmhandler, - "warning": mock_warning, - } + addHandler=mock_addhandler, + error=mock_error, + removeHandler=mock_rmhandler, + warning=mock_warning, ) if error: mock_rfhandler = mocker.patch( "solarwinds_apm.apm_logging.RotatingFileHandler", - side_effect=FileNotFoundError("mock error") + side_effect=FileNotFoundError("mock error"), ) else: mock_rfhandler = mocker.patch( @@ -64,7 +61,9 @@ def get_mock_logger_and_rfhandler( return mock_apm_logger, mock_rfhandler def test_update_sw_log_handler_no_log_filepath(self, mocker): - mock_apm_logger, mock_rfhandler = self.get_mock_logger_and_rfhandler(mocker) + mock_apm_logger, mock_rfhandler = self.get_mock_logger_and_rfhandler( + mocker + ) apm_logging.update_sw_log_handler("") mock_rfhandler.assert_not_called() @@ -81,7 +80,7 @@ def test_update_sw_log_handler_filenotfounderror(self, mocker): apm_logging.update_sw_log_handler("foo") mock_rfhandler.assert_called_once_with( - filename='foo', maxBytes=0, backupCount=0 + filename="foo", maxBytes=0, backupCount=0 ) mock_apm_logger.addHandler.assert_not_called() mock_apm_logger.removeHandler.assert_not_called() @@ -91,13 +90,15 @@ def test_update_sw_log_handler_filenotfounderror(self, mocker): mock_apm_logger.warning.assert_not_called() def test_update_sw_log_handler_update_handlers(self, mocker): - mock_apm_logger, mock_rfhandler = self.get_mock_logger_and_rfhandler(mocker) + mock_apm_logger, mock_rfhandler = self.get_mock_logger_and_rfhandler( + mocker + ) apm_logging.update_sw_log_handler("foo") mock_rfhandler.assert_called_once_with( - filename='foo', maxBytes=0, backupCount=0 + filename="foo", maxBytes=0, backupCount=0 ) mock_apm_logger.addHandler.assert_called_once() mock_apm_logger.removeHandler.assert_called_once() mock_apm_logger.error.assert_not_called() - mock_apm_logger.warning.assert_not_called() \ No newline at end of file + mock_apm_logger.warning.assert_not_called() diff --git a/tests/unit/test_apm_resource.py b/tests/unit/test_apm_resource.py index 09a1b7054..95f37ca4a 100644 --- a/tests/unit/test_apm_resource.py +++ b/tests/unit/test_apm_resource.py @@ -11,7 +11,6 @@ class TestCreateDetectorResource: - def test_create_detector_resource_calls_resource_create(self, mocker): mock_resource = Resource.create({"test.attr": "test-value"}) mock_resource_create = mocker.patch( @@ -27,7 +26,9 @@ def test_create_detector_resource_returns_resource(self): assert isinstance(result, Resource) assert hasattr(result, "attributes") - def test_create_detector_resource_includes_detector_attributes(self, mocker): + def test_create_detector_resource_includes_detector_attributes( + self, mocker + ): detector_attrs = { "process.pid": 12345, "process.executable.name": "python", @@ -45,27 +46,32 @@ def test_create_detector_resource_includes_detector_attributes(self, mocker): class TestCreateApmResource: - def test_create_apm_resource_adds_sw_attributes(self): detector_resource = Resource.create({"host.name": "test-host"}) service_name = "test-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes assert attrs["sw.apm.version"] == __version__ assert attrs["sw.data.module"] == "apm" assert attrs["service.name"] == service_name def test_create_apm_resource_preserves_detector_attributes(self): - detector_resource = Resource.create({ - "cloud.provider": "azure", - "cloud.resource_id": "/subscriptions/test/resourceGroups/test", - "host.name": "test-host", - "process.pid": 12345, - "k8s.namespace.name": "default", - "k8s.pod.name": "test-pod", - }) + detector_resource = Resource.create( + { + "cloud.provider": "azure", + "cloud.resource_id": "/subscriptions/test/resourceGroups/test", + "host.name": "test-host", + "process.pid": 12345, + "k8s.namespace.name": "default", + "k8s.pod.name": "test-pod", + } + ) service_name = "test-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes # SW attributes present @@ -74,20 +80,27 @@ def test_create_apm_resource_preserves_detector_attributes(self): assert attrs["service.name"] == service_name # All detector attributes preserved assert attrs["cloud.provider"] == "azure" - assert attrs["cloud.resource_id"] == "/subscriptions/test/resourceGroups/test" + assert ( + attrs["cloud.resource_id"] + == "/subscriptions/test/resourceGroups/test" + ) assert attrs["host.name"] == "test-host" assert attrs["process.pid"] == 12345 assert attrs["k8s.namespace.name"] == "default" assert attrs["k8s.pod.name"] == "test-pod" def test_create_apm_resource_overrides_detector_service_name(self): - detector_resource = Resource.create({ - "service.name": "detector-service", - "host.name": "test-host", - }) + detector_resource = Resource.create( + { + "service.name": "detector-service", + "host.name": "test-host", + } + ) service_name = "override-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes # Service name should be overridden @@ -99,7 +112,9 @@ def test_create_apm_resource_generates_service_instance_id(self): detector_resource = Resource.create({"host.name": "test-host"}) service_name = "test-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes assert "service.instance.id" in attrs @@ -112,19 +127,25 @@ def test_create_apm_resource_generates_service_instance_id(self): def test_create_apm_resource_preserves_existing_service_instance_id(self): existing_instance_id = "existing-instance-id-123" - detector_resource = Resource.create({ - "host.name": "test-host", - "service.instance.id": existing_instance_id, - }) + detector_resource = Resource.create( + { + "host.name": "test-host", + "service.instance.id": existing_instance_id, + } + ) service_name = "test-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes assert attrs["service.instance.id"] == existing_instance_id def test_create_apm_resource_with_empty_detector_resource(self): detector_resource = Resource.create() service_name = "test-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes assert attrs["sw.apm.version"] == __version__ assert attrs["sw.data.module"] == "apm" @@ -134,23 +155,29 @@ def test_create_apm_resource_with_empty_detector_resource(self): def test_create_apm_resource_with_empty_service_name(self): detector_resource = Resource.create({"host.name": "test-host"}) service_name = "" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes assert attrs["service.name"] == "" assert attrs["sw.apm.version"] == __version__ assert attrs["sw.data.module"] == "apm" def test_create_apm_resource_with_azure_detector_attributes(self): - detector_resource = Resource.create({ - "cloud.provider": "azure", - "cloud.platform": "azure_app_service", - "cloud.resource_id": "/subscriptions/sub-id/resourceGroups/rg-name/providers/Microsoft.Web/sites/app-name", - "service.name": "app-name", - "service.instance.id": "instance-123", - "host.id": "host-id-123", - }) + detector_resource = Resource.create( + { + "cloud.provider": "azure", + "cloud.platform": "azure_app_service", + "cloud.resource_id": "/subscriptions/sub-id/resourceGroups/rg-name/providers/Microsoft.Web/sites/app-name", + "service.name": "app-name", + "service.instance.id": "instance-123", + "host.id": "host-id-123", + } + ) service_name = "azure-app-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes # SW attributes assert attrs["sw.apm.version"] == __version__ @@ -159,24 +186,31 @@ def test_create_apm_resource_with_azure_detector_attributes(self): # Azure detector attributes preserved assert attrs["cloud.provider"] == "azure" assert attrs["cloud.platform"] == "azure_app_service" - assert attrs["cloud.resource_id"] == "/subscriptions/sub-id/resourceGroups/rg-name/providers/Microsoft.Web/sites/app-name" + assert ( + attrs["cloud.resource_id"] + == "/subscriptions/sub-id/resourceGroups/rg-name/providers/Microsoft.Web/sites/app-name" + ) assert attrs["host.id"] == "host-id-123" # Existing service.instance.id preserved assert attrs["service.instance.id"] == "instance-123" def test_create_apm_resource_with_k8s_detector_attributes(self): - detector_resource = Resource.create({ - "k8s.cluster.name": "test-cluster", - "k8s.namespace.name": "default", - "k8s.pod.name": "test-pod-12345", - "k8s.pod.uid": "pod-uid-12345", - "k8s.deployment.name": "test-deployment", - "k8s.node.name": "node-1", - "container.id": "container-id-12345", - "container.name": "test-container", - }) + detector_resource = Resource.create( + { + "k8s.cluster.name": "test-cluster", + "k8s.namespace.name": "default", + "k8s.pod.name": "test-pod-12345", + "k8s.pod.uid": "pod-uid-12345", + "k8s.deployment.name": "test-deployment", + "k8s.node.name": "node-1", + "container.id": "container-id-12345", + "container.name": "test-container", + } + ) service_name = "k8s-service" - result = apm_resource.create_apm_resource(detector_resource, service_name) + result = apm_resource.create_apm_resource( + detector_resource, service_name + ) attrs = result.attributes # SW attributes diff --git a/tests/unit/test_configurator/conftest.py b/tests/unit/test_configurator/conftest.py index 4f273b37c..cdc4dd2f9 100644 --- a/tests/unit/test_configurator/conftest.py +++ b/tests/unit/test_configurator/conftest.py @@ -12,6 +12,7 @@ # Configurator stdlib fixtures # ================================================================== + @pytest.fixture(name="mock_sys") def mock_sys(mocker): mock_version_info = mocker.PropertyMock() @@ -21,9 +22,7 @@ def mock_sys(mocker): mock_exec = mocker.PropertyMock() mock_exec.return_value = "/foo/path" - mock_sys = mocker.patch( - "solarwinds_apm.configurator.sys" - ) + mock_sys = mocker.patch("solarwinds_apm.configurator.sys") type(mock_sys).version_info = mock_version_info type(mock_sys).version = mock_version type(mock_sys).executable = mock_exec @@ -32,6 +31,7 @@ def mock_sys(mocker): return mock_sys + @pytest.fixture(name="mock_sys_error_version_info") def mock_sys_error_version_info(mocker): mock_version_info = mocker.PropertyMock() @@ -41,9 +41,7 @@ def mock_sys_error_version_info(mocker): mock_exec = mocker.PropertyMock() mock_exec.return_value = "/foo/path" - mock_sys = mocker.patch( - "solarwinds_apm.configurator.sys" - ) + mock_sys = mocker.patch("solarwinds_apm.configurator.sys") type(mock_sys).version_info = mock_version_info type(mock_sys).version = mock_version type(mock_sys).executable = mock_exec @@ -52,28 +50,33 @@ def mock_sys_error_version_info(mocker): return mock_sys + # ================================================================== # Configurator Otel fixtures # ================================================================== + @pytest.fixture(name="mock_set_global_textmap") def mock_set_global_textmap(mocker): return mocker.patch( "solarwinds_apm.configurator.set_global_textmap", ) + @pytest.fixture(name="mock_set_global_response_propagator") def mock_set_global_response_propagator(mocker): return mocker.patch( "solarwinds_apm.configurator.set_global_response_propagator", ) + @pytest.fixture(name="mock_composite_propagator") def mock_composite_propagator(mocker): return mocker.patch( "solarwinds_apm.configurator.CompositePropagator", ) + @pytest.fixture(name="mock_bsprocessor") def mock_bsprocessor(mocker): mock_bsp = mocker.patch( @@ -82,29 +85,32 @@ def mock_bsprocessor(mocker): mock_bsp.return_value = mocker.Mock() return mock_bsp + @pytest.fixture(name="mock_ssprocessor") def mock_ssprocessor(mocker): return mocker.patch( "solarwinds_apm.configurator.SimpleSpanProcessor", ) + @pytest.fixture(name="mock_pemreader") def mock_pemreader(mocker): return mocker.patch( "solarwinds_apm.configurator.PeriodicExportingMetricReader", ) + @pytest.fixture(name="mock_blprocessor") def mock_blprocessor(mocker): return mocker.patch( "solarwinds_apm.configurator.BatchLogRecordProcessor", ) + @pytest.fixture(name="mock_tracerprovider") def mock_tracerprovider(mocker): - return mocker.patch( - "solarwinds_apm.configurator.SolarwindsTracerProvider" - ) + return mocker.patch("solarwinds_apm.configurator.SolarwindsTracerProvider") + @pytest.fixture(name="mock_meterprovider") def mock_meterprovider(mocker): @@ -112,22 +118,26 @@ def mock_meterprovider(mocker): "solarwinds_apm.configurator.MeterProvider", ) + @pytest.fixture(name="mock_loggerprovider") def mock_loggerprovider(mocker): return mocker.patch( "solarwinds_apm.configurator.LoggerProvider", ) + @pytest.fixture(name="mock_logginghandler") def mock_logginghandler(mocker): return mocker.patch( "solarwinds_apm.configurator.LoggingHandler", ) + # ================================================================== # Configurator APM Python ApmConfig mocks # ================================================================== + def get_apmconfig_mocks( mocker, enabled=True, @@ -144,17 +154,16 @@ def get_side_effect(param): mock_apmconfig = mocker.Mock() mock_apmconfig.configure_mock( - **{ - "agent_enabled": enabled, - "get": mocker.Mock(side_effect=get_side_effect), - "service_name": "foo-service", - "is_lambda": is_lambda, - "oboe_api": mocker.Mock(), - "convert_to_bool": SolarWindsApmConfig.convert_to_bool, - } + agent_enabled=enabled, + get=mocker.Mock(side_effect=get_side_effect), + service_name="foo-service", + is_lambda=is_lambda, + oboe_api=mocker.Mock(), + convert_to_bool=SolarWindsApmConfig.convert_to_bool, ) return mock_apmconfig + @pytest.fixture(name="mock_apmconfig_disabled") def mock_apmconfig_disabled(mocker): return mocker.patch( @@ -162,18 +171,20 @@ def mock_apmconfig_disabled(mocker): get_apmconfig_mocks( mocker, enabled=False, - ) + ), ) + @pytest.fixture(name="mock_apmconfig_enabled") def mock_apmconfig_enabled(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsApmConfig", get_apmconfig_mocks( mocker, - ) + ), ) + @pytest.fixture(name="mock_apmconfig_enabled_is_lambda") def mock_apmconfig_enabled_is_lambda(mocker): return mocker.patch( @@ -181,9 +192,10 @@ def mock_apmconfig_enabled_is_lambda(mocker): get_apmconfig_mocks( mocker, is_lambda=True, - ) + ), ) + @pytest.fixture(name="mock_apmconfig_metrics_enabled_false") def mock_apmconfig_metrics_enabled_false(mocker): return mocker.patch( @@ -191,9 +203,10 @@ def mock_apmconfig_metrics_enabled_false(mocker): get_apmconfig_mocks( mocker, export_metrics_enabled=False, - ) + ), ) + @pytest.fixture(name="mock_apmconfig_metrics_enabled_none") def mock_apmconfig_metrics_enabled_none(mocker): return mocker.patch( @@ -201,67 +214,73 @@ def mock_apmconfig_metrics_enabled_none(mocker): get_apmconfig_mocks( mocker, export_metrics_enabled=None, - ) + ), ) + @pytest.fixture(name="mock_apmconfig_enabled_reporter_settings") def mock_apmconfig_enabled_reporter_settings(mocker): mock_apmconfig = mocker.Mock() mock_apmconfig.configure_mock( - **{ - "agent_enabled": True, - "certificates": "foo-certs", - "get": mocker.Mock(return_value="foo"), - "service_name": "foo-service", - "metric_format": "bar" - } + agent_enabled=True, + certificates="foo-certs", + get=mocker.Mock(return_value="foo"), + service_name="foo-service", + metric_format="bar", ) return mock_apmconfig + # ================================================================== # Configurator APM Python configurator mocks # ================================================================== + def add_fw_versions(input_dict): input_dict.update({"foo-fw": "bar-version"}) return input_dict + @pytest.fixture(name="mock_fw_versions") def mock_fw_versions(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._add_all_instrumented_python_framework_versions", - side_effect=add_fw_versions + side_effect=add_fw_versions, ) + @pytest.fixture(name="mock_config_serviceentry_processor") def mock_config_serviceentry_processor(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._configure_service_entry_span_processor" ) + @pytest.fixture(name="mock_response_time_processor") def mock_response_time_processor(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._configure_response_time_processor" ) + @pytest.fixture(name="mock_custom_init_tracing") def mock_custom_init_tracing(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._custom_init_tracing" ) + @pytest.fixture(name="mock_custom_init_metrics") def mock_custom_init_metrics(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._custom_init_metrics" ) + @pytest.fixture(name="mock_init_logging") def mock_init_logging(mocker): - return mocker.patch( - "solarwinds_apm.configurator._init_logging" - ) + return mocker.patch("solarwinds_apm.configurator._init_logging") + @pytest.fixture(name="mock_config_propagator") def mock_config_propagator(mocker): @@ -269,24 +288,28 @@ def mock_config_propagator(mocker): "solarwinds_apm.configurator.SolarWindsConfigurator._configure_propagator" ) + @pytest.fixture(name="mock_config_response_propagator") def mock_config_response_propagator(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._configure_response_propagator" ) + @pytest.fixture(name="mock_init_sw_reporter") def mock_init_sw_reporter(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._initialize_solarwinds_reporter" ) + @pytest.fixture(name="mock_create_init") def mock_create_init(mocker): return mocker.patch( "solarwinds_apm.configurator.SolarWindsConfigurator._create_init_event" ) + @pytest.fixture(name="mock_create_init_fail") def mock_create_init_fail(mocker): return mocker.patch( @@ -294,10 +317,12 @@ def mock_create_init_fail(mocker): return_value=None, ) + # ================================================================== # Configurator APM Python other mocks # ================================================================== + @pytest.fixture(name="mock_apm_version") def mock_apm_version(mocker): return mocker.patch( diff --git a/tests/unit/test_configurator/fixtures/logging.py b/tests/unit/test_configurator/fixtures/logging.py index b6b396944..f0a7824c2 100644 --- a/tests/unit/test_configurator/fixtures/logging.py +++ b/tests/unit/test_configurator/fixtures/logging.py @@ -4,20 +4,11 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + def get_logging_mocks(mocker): mock_logger = mocker.Mock() - mock_logger.configure_mock( - **{ - "addHandler": mocker.Mock() - } - ) + mock_logger.configure_mock(addHandler=mocker.Mock()) mock_get_logger = mocker.Mock(return_value=mock_logger) - mock_logging = mocker.patch( - "solarwinds_apm.configurator.logging" - ) - mock_logging.configure_mock( - **{ - "getLogger": mock_get_logger - } - ) + mock_logging = mocker.patch("solarwinds_apm.configurator.logging") + mock_logging.configure_mock(getLogger=mock_get_logger) return mock_logging diff --git a/tests/unit/test_configurator/fixtures/resource.py b/tests/unit/test_configurator/fixtures/resource.py index 72f0d871d..b34e0ef08 100644 --- a/tests/unit/test_configurator/fixtures/resource.py +++ b/tests/unit/test_configurator/fixtures/resource.py @@ -4,19 +4,12 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + def get_resource_mocks(mocker): mock_new_res = mocker.Mock() - mock_new_res.configure_mock( - **{ - "merge": {"foo-merged": "yay"} - } - ) + mock_new_res.configure_mock(merge={"foo-merged": "yay"}) mock_res = mocker.patch( "solarwinds_apm.configurator.Resource", ) - mock_res.configure_mock( - **{ - "create": mock_new_res - } - ) + mock_res.configure_mock(create=mock_new_res) return mock_res diff --git a/tests/unit/test_configurator/fixtures/trace.py b/tests/unit/test_configurator/fixtures/trace.py index baa38e9e5..2e579665f 100644 --- a/tests/unit/test_configurator/fixtures/trace.py +++ b/tests/unit/test_configurator/fixtures/trace.py @@ -4,9 +4,10 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + def get_trace_mocks(mocker): mock_add_span_processor = mocker.Mock() - + mock_attributes = mocker.PropertyMock() mock_attributes.return_value = {"foo": "bar"} mock_resource = mocker.PropertyMock() @@ -17,24 +18,17 @@ def get_trace_mocks(mocker): mock_tracer_provider = mocker.Mock() mock_tracer_provider.configure_mock( - **{ - "add_span_processor": mock_add_span_processor, - "get_tracer": mock_get_tracer, - } - ) - mock_get_tracer_provider = mocker.Mock( - return_value=mock_tracer_provider + add_span_processor=mock_add_span_processor, get_tracer=mock_get_tracer ) + mock_get_tracer_provider = mocker.Mock(return_value=mock_tracer_provider) mock_set_tracer_provider = mocker.Mock() mock_noop_tracer_provider = mocker.Mock() mock_trace = mocker.patch( "solarwinds_apm.configurator.trace", ) mock_trace.configure_mock( - **{ - "get_tracer_provider": mock_get_tracer_provider, - "set_tracer_provider": mock_set_tracer_provider, - "NoOpTracerProvider": mock_noop_tracer_provider, - } + get_tracer_provider=mock_get_tracer_provider, + set_tracer_provider=mock_set_tracer_provider, + NoOpTracerProvider=mock_noop_tracer_provider, ) return mock_trace diff --git a/tests/unit/test_configurator/test_configurator_configure_otel.py b/tests/unit/test_configurator/test_configurator_configure_otel.py index 507565eb8..79a0a9340 100644 --- a/tests/unit/test_configurator/test_configurator_configure_otel.py +++ b/tests/unit/test_configurator/test_configurator_configure_otel.py @@ -5,8 +5,8 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. import logging -import pytest +import pytest from solarwinds_apm import configurator @@ -22,7 +22,6 @@ def test_configure_otel_components_agent_enabled( self, mocker, mock_apmconfig_enabled, - mock_config_serviceentry_processor, mock_response_time_processor, mock_custom_init_tracing, @@ -76,7 +75,6 @@ def test_configure_otel_components_agent_disabled( self, mocker, mock_apmconfig_disabled, - mock_config_serviceentry_processor, mock_response_time_processor, mock_custom_init_tracing, @@ -109,4 +107,4 @@ def test_configure_otel_components_agent_disabled( mock_custom_init_metrics.assert_not_called() mock_init_logging.assert_not_called() mock_config_propagator.assert_not_called() - mock_config_response_propagator.assert_not_called() \ No newline at end of file + mock_config_response_propagator.assert_not_called() diff --git a/tests/unit/test_configurator/test_configurator_metrics_init.py b/tests/unit/test_configurator/test_configurator_metrics_init.py index 498cecfe7..ab0bba63d 100644 --- a/tests/unit/test_configurator/test_configurator_metrics_init.py +++ b/tests/unit/test_configurator/test_configurator_metrics_init.py @@ -59,15 +59,11 @@ def test_custom_init_metrics_exporter_not_is_lambda( "solarwinds_apm.configurator.Resource.create", return_value=mock_resource, ) - mock_counter = mocker.patch( - "solarwinds_apm.configurator.Counter" - ) + mock_counter = mocker.patch("solarwinds_apm.configurator.Counter") mock_updowncounter = mocker.patch( "solarwinds_apm.configurator.UpDownCounter" ) - mock_histogram = mocker.patch( - "solarwinds_apm.configurator.Histogram" - ) + mock_histogram = mocker.patch("solarwinds_apm.configurator.Histogram") mock_observablecounter = mocker.patch( "solarwinds_apm.configurator.ObservableCounter" ) @@ -89,10 +85,10 @@ def __init__(self, *args, **kwargs): def export(self): pass - + def force_flush(self, *args, **kwargs): pass - + def shutdown(self, *args, **kwargs): pass @@ -144,15 +140,11 @@ def test_custom_init_metrics_exporter_is_lambda( "solarwinds_apm.configurator.Resource.create", return_value=mock_resource, ) - mock_counter = mocker.patch( - "solarwinds_apm.configurator.Counter" - ) + mock_counter = mocker.patch("solarwinds_apm.configurator.Counter") mock_updowncounter = mocker.patch( "solarwinds_apm.configurator.UpDownCounter" ) - mock_histogram = mocker.patch( - "solarwinds_apm.configurator.Histogram" - ) + mock_histogram = mocker.patch("solarwinds_apm.configurator.Histogram") mock_observablecounter = mocker.patch( "solarwinds_apm.configurator.ObservableCounter" ) @@ -174,10 +166,10 @@ def __init__(self, *args, **kwargs): def export(self): pass - + def force_flush(self, *args, **kwargs): pass - + def shutdown(self, *args, **kwargs): pass @@ -230,15 +222,11 @@ def test_custom_init_metrics_reader_not_is_lambda( "solarwinds_apm.configurator.Resource.create", return_value=mock_resource, ) - mock_counter = mocker.patch( - "solarwinds_apm.configurator.Counter" - ) + mock_counter = mocker.patch("solarwinds_apm.configurator.Counter") mock_updowncounter = mocker.patch( "solarwinds_apm.configurator.UpDownCounter" ) - mock_histogram = mocker.patch( - "solarwinds_apm.configurator.Histogram" - ) + mock_histogram = mocker.patch("solarwinds_apm.configurator.Histogram") mock_observablecounter = mocker.patch( "solarwinds_apm.configurator.ObservableCounter" ) @@ -310,15 +298,11 @@ def test_custom_init_metrics_reader_is_lambda( "solarwinds_apm.configurator.Resource.create", return_value=mock_resource, ) - mock_counter = mocker.patch( - "solarwinds_apm.configurator.Counter" - ) + mock_counter = mocker.patch("solarwinds_apm.configurator.Counter") mock_updowncounter = mocker.patch( "solarwinds_apm.configurator.UpDownCounter" ) - mock_histogram = mocker.patch( - "solarwinds_apm.configurator.Histogram" - ) + mock_histogram = mocker.patch("solarwinds_apm.configurator.Histogram") mock_observablecounter = mocker.patch( "solarwinds_apm.configurator.ObservableCounter" ) diff --git a/tests/unit/test_configurator/test_configurator_propagators.py b/tests/unit/test_configurator/test_configurator_propagators.py index dee3d47e7..47134d4ed 100644 --- a/tests/unit/test_configurator/test_configurator_propagators.py +++ b/tests/unit/test_configurator/test_configurator_propagators.py @@ -26,11 +26,7 @@ def test_configure_propagator_none_uses_default( # Mock entry points mock_propagator_class = mocker.MagicMock() mock_propagator_entry_point = mocker.Mock() - mock_propagator_entry_point.configure_mock( - **{ - "load": mock_propagator_class - } - ) + mock_propagator_entry_point.configure_mock(load=mock_propagator_class) mock_points = iter( [ mock_propagator_entry_point, @@ -41,9 +37,7 @@ def test_configure_propagator_none_uses_default( mock_entry_points = mocker.patch( "solarwinds_apm.configurator.entry_points" ) - mock_entry_points.configure_mock( - return_value=mock_points - ) + mock_entry_points.configure_mock(return_value=mock_points) # Test! test_configurator = configurator.SolarWindsConfigurator() @@ -54,10 +48,7 @@ def test_configure_propagator_none_uses_default( group="opentelemetry_propagator", name="solarwinds_propagator", ), - mocker.call( - group="opentelemetry_propagator", - name="baggage" - ), + mocker.call(group="opentelemetry_propagator", name="baggage"), ] ) mock_composite_propagator.assert_called_once() @@ -79,10 +70,7 @@ def test_configure_propagator_invalid( del os.environ["OTEL_PROPAGATORS"] mocker.patch.dict( - os.environ, - { - "OTEL_PROPAGATORS": "invalid_propagator" - } + os.environ, {"OTEL_PROPAGATORS": "invalid_propagator"} ) # Test! @@ -107,11 +95,7 @@ def test_configure_propagator_valid( # Mock entry points mock_propagator_class = mocker.MagicMock() mock_propagator_entry_point = mocker.Mock() - mock_propagator_entry_point.configure_mock( - **{ - "load": mock_propagator_class - } - ) + mock_propagator_entry_point.configure_mock(load=mock_propagator_class) mock_points = iter( [ mock_propagator_entry_point, @@ -120,16 +104,9 @@ def test_configure_propagator_valid( mock_entry_points = mocker.patch( "solarwinds_apm.configurator.entry_points" ) - mock_entry_points.configure_mock( - return_value=mock_points - ) + mock_entry_points.configure_mock(return_value=mock_points) - mocker.patch.dict( - os.environ, - { - "OTEL_PROPAGATORS": "valid_propagator" - } - ) + mocker.patch.dict(os.environ, {"OTEL_PROPAGATORS": "valid_propagator"}) # Test! test_configurator = configurator.SolarWindsConfigurator() @@ -137,8 +114,7 @@ def test_configure_propagator_valid( mock_entry_points.assert_has_calls( [ mocker.call( - group="opentelemetry_propagator", - name="valid_propagator" + group="opentelemetry_propagator", name="valid_propagator" ), ] ) @@ -163,11 +139,7 @@ def test_configure_propagator_valid_baggage_sw( # Mock entry points mock_propagator_class = mocker.MagicMock() mock_propagator_entry_point = mocker.Mock() - mock_propagator_entry_point.configure_mock( - **{ - "load": mock_propagator_class - } - ) + mock_propagator_entry_point.configure_mock(load=mock_propagator_class) mock_points = iter( [ mock_propagator_entry_point, @@ -178,15 +150,10 @@ def test_configure_propagator_valid_baggage_sw( mock_entry_points = mocker.patch( "solarwinds_apm.configurator.entry_points" ) - mock_entry_points.configure_mock( - return_value=mock_points - ) + mock_entry_points.configure_mock(return_value=mock_points) mocker.patch.dict( - os.environ, - { - "OTEL_PROPAGATORS": "baggage,solarwinds_propagator" - } + os.environ, {"OTEL_PROPAGATORS": "baggage,solarwinds_propagator"} ) # Test! @@ -194,13 +161,10 @@ def test_configure_propagator_valid_baggage_sw( test_configurator._configure_propagator() mock_entry_points.assert_has_calls( [ + mocker.call(group="opentelemetry_propagator", name="baggage"), mocker.call( group="opentelemetry_propagator", - name="baggage" - ), - mocker.call( - group="opentelemetry_propagator", - name="solarwinds_propagator" + name="solarwinds_propagator", ), ] ) @@ -225,11 +189,7 @@ def test_configure_propagator_valid_sw_baggage( # Mock entry points mock_propagator_class = mocker.MagicMock() mock_propagator_entry_point = mocker.Mock() - mock_propagator_entry_point.configure_mock( - **{ - "load": mock_propagator_class - } - ) + mock_propagator_entry_point.configure_mock(load=mock_propagator_class) mock_points = iter( [ mock_propagator_entry_point, @@ -240,15 +200,10 @@ def test_configure_propagator_valid_sw_baggage( mock_entry_points = mocker.patch( "solarwinds_apm.configurator.entry_points" ) - mock_entry_points.configure_mock( - return_value=mock_points - ) + mock_entry_points.configure_mock(return_value=mock_points) mocker.patch.dict( - os.environ, - { - "OTEL_PROPAGATORS": "solarwinds_propagator,baggage" - } + os.environ, {"OTEL_PROPAGATORS": "solarwinds_propagator,baggage"} ) # Test! @@ -258,12 +213,9 @@ def test_configure_propagator_valid_sw_baggage( [ mocker.call( group="opentelemetry_propagator", - name="solarwinds_propagator" - ), - mocker.call( - group="opentelemetry_propagator", - name="baggage" + name="solarwinds_propagator", ), + mocker.call(group="opentelemetry_propagator", name="baggage"), ] ) mock_composite_propagator.assert_called_once() @@ -284,43 +236,34 @@ def test_configure_propagator_valid_invalid_mixed( if old_propagators: del os.environ["OTEL_PROPAGATORS"] - # Mock entry points mock_propagator_class = mocker.MagicMock() mock_propagator_entry_point = mocker.Mock() - mock_propagator_entry_point.configure_mock( - **{ - "load": mock_propagator_class - } - ) + mock_propagator_entry_point.configure_mock(load=mock_propagator_class) mock_points = iter( [ mock_propagator_entry_point, - Exception("mock error invalid propagator") + Exception("mock error invalid propagator"), ] ) mock_entry_points = mocker.patch( "solarwinds_apm.configurator.entry_points" ) - mock_entry_points.configure_mock( - return_value=mock_points - ) + mock_entry_points.configure_mock(return_value=mock_points) mocker.patch.dict( os.environ, - { - "OTEL_PROPAGATORS": "valid_propagator,invalid_propagator" - } + {"OTEL_PROPAGATORS": "valid_propagator,invalid_propagator"}, ) # Test! test_configurator = configurator.SolarWindsConfigurator() - with pytest.raises(Exception): + with pytest.raises(AttributeError): test_configurator._configure_propagator() mock_entry_points.assert_has_calls( [ mocker.call( - group="opentelemetry_propagator", + group="opentelemetry_propagator", name="valid_propagator", ), mocker.call( @@ -332,7 +275,6 @@ def test_configure_propagator_valid_invalid_mixed( mock_composite_propagator.assert_not_called() mock_set_global_textmap.assert_not_called() - # Restore old PROPAGATOR if old_propagators: os.environ["OTEL_PROPAGATORS"] = old_propagators diff --git a/tests/unit/test_configurator/test_configurator_span_processors.py b/tests/unit/test_configurator/test_configurator_span_processors.py index c9df22dfa..a5b1560bc 100644 --- a/tests/unit/test_configurator/test_configurator_span_processors.py +++ b/tests/unit/test_configurator/test_configurator_span_processors.py @@ -8,6 +8,7 @@ from solarwinds_apm import configurator + class TestConfiguratorSpanProcessors: def test_configure_service_entry_span_processor( self, @@ -18,9 +19,7 @@ def test_configure_service_entry_span_processor( "solarwinds_apm.configurator.trace.get_tracer_provider", return_value=mock_tracerprovider, ) - mocker.patch( - "solarwinds_apm.configurator.ServiceEntrySpanProcessor" - ) + mocker.patch("solarwinds_apm.configurator.ServiceEntrySpanProcessor") test_configurator = configurator.SolarWindsConfigurator() test_configurator._configure_service_entry_span_processor() @@ -36,9 +35,12 @@ def test_configure_response_time_processor_exporters_not_set( old_exporter = os.environ.get("OTEL_METRICS_EXPORTER", None) if old_exporter: del os.environ["OTEL_METRICS_EXPORTER"] - mocker.patch.dict(os.environ, { - "OTEL_METRICS_EXPORTER": "", - }) + mocker.patch.dict( + os.environ, + { + "OTEL_METRICS_EXPORTER": "", + }, + ) mock_tracerprovider = mocker.Mock() mock_get_tracer_provider = mocker.patch( @@ -70,9 +72,12 @@ def test_configure_response_time_processor_exporters_set( old_exporter = os.environ.get("OTEL_METRICS_EXPORTER", None) if old_exporter: del os.environ["OTEL_METRICS_EXPORTER"] - mocker.patch.dict(os.environ, { - "OTEL_METRICS_EXPORTER": "foo_exporter", - }) + mocker.patch.dict( + os.environ, + { + "OTEL_METRICS_EXPORTER": "foo_exporter", + }, + ) mock_tracerprovider = mocker.Mock() mock_get_tracer_provider = mocker.patch( @@ -95,4 +100,4 @@ def test_configure_response_time_processor_exporters_set( # Restore the os exporters if old_exporter: - os.environ["OTEL_METRICS_EXPORTER"] = old_exporter \ No newline at end of file + os.environ["OTEL_METRICS_EXPORTER"] = old_exporter diff --git a/tests/unit/test_configurator/test_configurator_traces_init.py b/tests/unit/test_configurator/test_configurator_traces_init.py index f773693e4..f9a4ff0c8 100644 --- a/tests/unit/test_configurator/test_configurator_traces_init.py +++ b/tests/unit/test_configurator/test_configurator_traces_init.py @@ -22,7 +22,9 @@ def setup_each_test( if otlp_protocol: env_vars["OTEL_EXPORTER_OTLP_PROTOCOL"] = otlp_protocol if otlp_traces_protocol: - env_vars["OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"] = otlp_traces_protocol + env_vars["OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"] = ( + otlp_traces_protocol + ) mocker.patch.dict(os.environ, env_vars, clear=True) mocker.patch( @@ -79,7 +81,9 @@ def test_custom_init_tracing_none( resource=mocks["mock_resource"], ) mocks["mock_set_tracer_provider"].assert_called_once() - mocks["mock_tracerprovider_instance"].add_span_processor.assert_not_called() + mocks[ + "mock_tracerprovider_instance" + ].add_span_processor.assert_not_called() mock_bsprocessor.assert_not_called() mock_ssprocessor.assert_not_called() @@ -96,7 +100,7 @@ def test_custom_init_tracing_not_is_lambda( ) # Mock span exporter class - class MockExporter(): + class MockExporter: def __init__(self, *args, **kwargs): pass @@ -117,7 +121,9 @@ def __init__(self, *args, **kwargs): resource=mocks["mock_apm_sampler"], ) mocks["mock_set_tracer_provider"].assert_called_once() - mocks["mock_tracerprovider_instance"].add_span_processor.assert_called_once_with( + mocks[ + "mock_tracerprovider_instance" + ].add_span_processor.assert_called_once_with( mock_bsprocessor.return_value, ) mock_exporter_spy.assert_called_once() @@ -137,7 +143,7 @@ def test_custom_init_tracing_is_lambda( ) # Mock span exporter class - class MockExporter(): + class MockExporter: def __init__(self, *args, **kwargs): pass @@ -158,7 +164,9 @@ def __init__(self, *args, **kwargs): resource=mocks["mock_resource"], ) mocks["mock_set_tracer_provider"].assert_called_once() - mocks["mock_tracerprovider_instance"].add_span_processor.assert_called_once_with( + mocks[ + "mock_tracerprovider_instance" + ].add_span_processor.assert_called_once_with( mock_ssprocessor.return_value, ) mock_exporter_spy.assert_called_once() diff --git a/tests/unit/test_distro.py b/tests/unit/test_distro.py index 10aeb5526..5ccd0b79f 100644 --- a/tests/unit/test_distro.py +++ b/tests/unit/test_distro.py @@ -5,13 +5,13 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. import os -import pytest +import pytest from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, OTEL_METRICS_EXPORTER, OTEL_PROPAGATORS, - OTEL_TRACES_EXPORTER + OTEL_TRACES_EXPORTER, ) from opentelemetry.instrumentation.logging.environment_variables import ( OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION, @@ -46,7 +46,8 @@ def test_new_initializes_class_variables(self, mocker): return_value={"foo": "bar"}, ) mock_calculate_metrics_enabled = mocker.patch( - "solarwinds_apm.distro.SolarWindsApmConfig.calculate_metrics_enabled", return_value="qux", + "solarwinds_apm.distro.SolarWindsApmConfig.calculate_metrics_enabled", + return_value="qux", ) instance = distro.SolarWindsDistro() @@ -56,37 +57,17 @@ def test_new_initializes_class_variables(self, mocker): mock_calculate_metrics_enabled.assert_called_once_with({"foo": "bar"}) def test__log_python_runtime(self, mocker): - mock_plat = mocker.patch( - "solarwinds_apm.distro.platform" - ) + mock_plat = mocker.patch("solarwinds_apm.distro.platform") mock_py_vers = mocker.Mock() - mock_plat.configure_mock( - **{ - "python_version": mock_py_vers - } - ) - mock_sys = mocker.patch( - "solarwinds_apm.distro.sys" - ) + mock_plat.configure_mock(python_version=mock_py_vers) + mock_sys = mocker.patch("solarwinds_apm.distro.sys") mock_version_info = mocker.Mock() - mock_version_info.configure_mock( - **{ - "major": 3, - "minor": 8, - } - ) + mock_version_info.configure_mock(major=3, minor=8) type(mock_sys).version_info = mock_version_info - mock_logger = mocker.patch( - "solarwinds_apm.distro.logger" - ) + mock_logger = mocker.patch("solarwinds_apm.distro.logger") mock_info = mocker.Mock() mock_warning = mocker.Mock() - mock_logger.configure_mock( - **{ - "info": mock_info, - "warning": mock_warning, - } - ) + mock_logger.configure_mock(info=mock_info, warning=mock_warning) distro.SolarWindsDistro()._log_python_runtime() mock_py_vers.assert_called_once() @@ -94,37 +75,17 @@ def test__log_python_runtime(self, mocker): mock_warning.assert_not_called() def test__log_python_runtime_warning(self, mocker): - mock_plat = mocker.patch( - "solarwinds_apm.distro.platform" - ) + mock_plat = mocker.patch("solarwinds_apm.distro.platform") mock_py_vers = mocker.Mock() - mock_plat.configure_mock( - **{ - "python_version": mock_py_vers - } - ) - mock_sys = mocker.patch( - "solarwinds_apm.distro.sys" - ) + mock_plat.configure_mock(python_version=mock_py_vers) + mock_sys = mocker.patch("solarwinds_apm.distro.sys") mock_version_info = mocker.Mock() - mock_version_info.configure_mock( - **{ - "major": 3, - "minor": 7, - } - ) + mock_version_info.configure_mock(major=3, minor=7) type(mock_sys).version_info = mock_version_info - mock_logger = mocker.patch( - "solarwinds_apm.distro.logger" - ) + mock_logger = mocker.patch("solarwinds_apm.distro.logger") mock_info = mocker.Mock() mock_error = mocker.Mock() - mock_logger.configure_mock( - **{ - "info": mock_info, - "error": mock_error, - } - ) + mock_logger.configure_mock(info=mock_info, error=mock_error) distro.SolarWindsDistro()._log_python_runtime() mock_py_vers.assert_called_once() @@ -144,15 +105,9 @@ def test__log_runtime(self, mocker): "solarwinds_apm.distro.inst_version", "baz-version", ) - mock_logger = mocker.patch( - "solarwinds_apm.distro.logger" - ) + mock_logger = mocker.patch("solarwinds_apm.distro.logger") mock_info = mocker.Mock() - mock_logger.configure_mock( - **{ - "info": mock_info, - } - ) + mock_logger.configure_mock(info=mock_info) mock_pytime = mocker.patch( "solarwinds_apm.distro.SolarWindsDistro._log_python_runtime" ) @@ -179,26 +134,24 @@ def test__get_token_from_service_key_missing(self, mocker): def test__get_token_from_service_key_bad_format(self, mocker): mocker.patch.dict( - os.environ, - { - "SW_APM_SERVICE_KEY": "missing-service-name" - } + os.environ, {"SW_APM_SERVICE_KEY": "missing-service-name"} ) assert distro.SolarWindsDistro()._get_token_from_service_key() is None def test__get_token_from_service_key_ok(self, mocker): mocker.patch.dict( - os.environ, - { - "SW_APM_SERVICE_KEY": "foo-token:bar-name" - } + os.environ, {"SW_APM_SERVICE_KEY": "foo-token:bar-name"} + ) + assert ( + distro.SolarWindsDistro()._get_token_from_service_key() + == "foo-token" ) - assert distro.SolarWindsDistro()._get_token_from_service_key() == "foo-token" def test__get_token_from_service_key_missing_logs_in_non_lambda_mode( self, mocker, caplog ): import logging + caplog.set_level(logging.DEBUG, logger="solarwinds_apm.distro") # Non-Lambda environment (no AWS_LAMBDA_FUNCTION_NAME) mocker.patch.dict( @@ -219,6 +172,7 @@ def test__get_token_from_service_key_missing_no_log_in_lambda_mode( self, mocker, caplog ): import logging + caplog.set_level(logging.DEBUG, logger="solarwinds_apm.distro") mocker.patch.dict( os.environ, @@ -249,22 +203,36 @@ def test_configure_no_env(self, mocker): assert os.environ[OTEL_METRICS_EXPORTER] == "otlp" assert os.environ[OTEL_LOGS_EXPORTER] == "otlp" assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "http/protobuf" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20None" - assert os.environ[OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION] == "base2_exponential_bucket_histogram" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20None" + ) + assert ( + os.environ[ + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION + ] + == "base2_exponential_bucket_histogram" + ) assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_exporter(self, mocker): mocker.patch.dict( - os.environ, - { - "SW_APM_SERVICE_KEY": "foo-token:bar", - "OTEL_TRACES_EXPORTER": "foobar", - "OTEL_METRICS_EXPORTER": "baz", - "OTEL_LOGS_EXPORTER": "qux", - } + os.environ, + { + "SW_APM_SERVICE_KEY": "foo-token:bar", + "OTEL_TRACES_EXPORTER": "foobar", + "OTEL_METRICS_EXPORTER": "baz", + "OTEL_LOGS_EXPORTER": "qux", + }, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" @@ -272,77 +240,113 @@ def test_configure_env_exporter(self, mocker): assert os.environ[OTEL_METRICS_EXPORTER] == "baz" assert os.environ[OTEL_LOGS_EXPORTER] == "qux" assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "http/protobuf" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_invalid_protocol(self, mocker): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", - OTEL_EXPORTER_OTLP_PROTOCOL: "foo" + OTEL_EXPORTER_OTLP_PROTOCOL: "foo", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" # Invalid protocol still set; let upstream Configurator handle it assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "foo" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) assert os.environ[OTEL_TRACES_EXPORTER] == "otlp" assert os.environ[OTEL_METRICS_EXPORTER] == "otlp" assert os.environ[OTEL_LOGS_EXPORTER] == "otlp" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_valid_protocol_http(self, mocker): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", - OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf" + OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "http/protobuf" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) assert os.environ[OTEL_TRACES_EXPORTER] == "otlp" assert os.environ[OTEL_METRICS_EXPORTER] == "otlp" assert os.environ[OTEL_LOGS_EXPORTER] == "otlp" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_valid_protocol_grpc(self, mocker): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", - OTEL_EXPORTER_OTLP_PROTOCOL: "grpc" + OTEL_EXPORTER_OTLP_PROTOCOL: "grpc", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" # Distro will only setdefault PROTOCOL as grpc # and will still setdefault EXPORTERS as http like default distro assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "grpc" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) assert os.environ[OTEL_TRACES_EXPORTER] == "otlp" assert os.environ[OTEL_METRICS_EXPORTER] == "otlp" assert os.environ[OTEL_LOGS_EXPORTER] == "otlp" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_exporter_and_valid_protocol_http(self, mocker): mocker.patch.dict( @@ -354,19 +358,28 @@ def test_configure_env_exporter_and_valid_protocol_http(self, mocker): "OTEL_METRICS_EXPORTER": "baz", "OTEL_LOGS_EXPORTER": "qux", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "http/protobuf" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) assert os.environ[OTEL_TRACES_EXPORTER] == "foobar" assert os.environ[OTEL_METRICS_EXPORTER] == "baz" assert os.environ[OTEL_LOGS_EXPORTER] == "qux" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_exporter_and_valid_protocol_grpc(self, mocker): mocker.patch.dict( @@ -378,19 +391,28 @@ def test_configure_env_exporter_and_valid_protocol_grpc(self, mocker): "OTEL_METRICS_EXPORTER": "baz", "OTEL_LOGS_EXPORTER": "qux", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "grpc" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) assert os.environ[OTEL_TRACES_EXPORTER] == "foobar" assert os.environ[OTEL_METRICS_EXPORTER] == "baz" assert os.environ[OTEL_LOGS_EXPORTER] == "qux" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_endpoint(self, mocker): mocker.patch.dict( @@ -399,19 +421,25 @@ def test_configure_env_endpoint(self, mocker): "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://foo.bar.com:443", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "http/protobuf" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://foo.bar.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://foo.bar.com:443" + ) assert os.environ.get(OTEL_EXPORTER_OTLP_HEADERS) is None assert os.environ[OTEL_TRACES_EXPORTER] == "otlp" assert os.environ[OTEL_METRICS_EXPORTER] == "otlp" assert os.environ[OTEL_LOGS_EXPORTER] == "otlp" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_service_key_only(self, mocker): mocker.patch.dict( @@ -419,10 +447,13 @@ def test_configure_env_service_key_only(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) def test_configure_env_service_key_and_collector_only(self, mocker): mocker.patch.dict( @@ -431,10 +462,13 @@ def test_configure_env_service_key_and_collector_only(self, mocker): "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "apm.collector.na-02.cloud.solarwinds.com", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-02.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-02.cloud.solarwinds.com:443" + ) def test_configure_env_service_key_and_collector_only_dev(self, mocker): mocker.patch.dict( @@ -443,10 +477,13 @@ def test_configure_env_service_key_and_collector_only_dev(self, mocker): "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "apm.collector.na-01.dev-ssp.solarwinds.com", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.dev-ssp.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.dev-ssp.solarwinds.com:443" + ) def test_configure_env_service_key_and_collector_only_stg(self, mocker): mocker.patch.dict( @@ -455,36 +492,51 @@ def test_configure_env_service_key_and_collector_only_stg(self, mocker): "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "apm.collector.na-02.st-ssp.solarwinds.com", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-02.st-ssp.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-02.st-ssp.solarwinds.com:443" + ) - def test_configure_env_service_key_and_collector_only_invalid(self, mocker): + def test_configure_env_service_key_and_collector_only_invalid( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "www.google.com", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) - def test_configure_env_service_key_and_collector_only_invalid_otel(self, mocker): + def test_configure_env_service_key_and_collector_only_invalid_otel( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "otel.collector.na-02.cloud.solarwinds.com", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) - def test_configure_env_service_key_and_collector_and_otel_exporter(self, mocker): + def test_configure_env_service_key_and_collector_and_otel_exporter( + self, mocker + ): mocker.patch.dict( os.environ, { @@ -492,12 +544,17 @@ def test_configure_env_service_key_and_collector_and_otel_exporter(self, mocker) "SW_APM_COLLECTOR": "apm.collector.na-02.cloud.solarwinds.com", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.collector.na-03.cloud.solarwinds.com:443", }, - clear=True + clear=True, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-03.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-03.cloud.solarwinds.com:443" + ) - def test_configure_env_service_key_and_collector_from_file_no_env(self, mocker): + def test_configure_env_service_key_and_collector_from_file_no_env( + self, mocker + ): mocker.patch.dict( os.environ, { @@ -511,9 +568,14 @@ def test_configure_env_service_key_and_collector_from_file_no_env(self, mocker): }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + ) - def test_configure_env_service_key_and_collector_from_env_no_file(self, mocker): + def test_configure_env_service_key_and_collector_from_env_no_file( + self, mocker + ): mocker.patch.dict( os.environ, { @@ -525,9 +587,14 @@ def test_configure_env_service_key_and_collector_from_env_no_file(self, mocker): return_value={}, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.jp-01.st-ssp.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.jp-01.st-ssp.solarwinds.com:443" + ) - def test_configure_env_service_key_and_collector_from_env_and_file(self, mocker): + def test_configure_env_service_key_and_collector_from_env_and_file( + self, mocker + ): mocker.patch.dict( os.environ, { @@ -541,18 +608,27 @@ def test_configure_env_service_key_and_collector_from_env_and_file(self, mocker) }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.jp-01.st-ssp.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.jp-01.st-ssp.solarwinds.com:443" + ) def test_configure_env_headers_otel_endpoint_none(self, mocker): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) def test_configure_env_headers_otel_endpoint_default(self, mocker): mocker.patch.dict( @@ -560,11 +636,17 @@ def test_configure_env_headers_otel_endpoint_default(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.collector.na-01.cloud.solarwinds.com:443", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) def test_configure_env_headers_otel_endpoint_resolved(self, mocker): mocker.patch.dict( @@ -572,11 +654,17 @@ def test_configure_env_headers_otel_endpoint_resolved(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.collector.eu-01.st-ssp.solarwinds.com:443", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) def test_configure_env_headers_otel_endpoint_non_swo(self, mocker): mocker.patch.dict( @@ -584,10 +672,12 @@ def test_configure_env_headers_otel_endpoint_non_swo(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://my-collector/", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://my-collector/" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://my-collector/" + ) assert os.environ.get(OTEL_EXPORTER_OTLP_HEADERS) is None def test_configure_env_headers_otel_endpoint_fake_swo(self, mocker): @@ -596,10 +686,13 @@ def test_configure_env_headers_otel_endpoint_fake_swo(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.collector.solarwinds.com", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.solarwinds.com" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.solarwinds.com" + ) assert os.environ.get(OTEL_EXPORTER_OTLP_HEADERS) is None def test_configure_env_headers_otel_headers(self, mocker): @@ -608,19 +701,25 @@ def test_configure_env_headers_otel_headers(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_HEADERS": "foo=bar,baz=qux", - } + }, ) distro.SolarWindsDistro()._configure() assert os.environ[OTEL_PROPAGATORS] == "solarwinds_propagator,baggage" assert os.environ[OTEL_EXPORTER_OTLP_PROTOCOL] == "http/protobuf" - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "foo=bar,baz=qux" assert os.environ[OTEL_TRACES_EXPORTER] == "otlp" assert os.environ[OTEL_METRICS_EXPORTER] == "otlp" assert os.environ[OTEL_LOGS_EXPORTER] == "otlp" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_configure_env_headers_otel_headers_endpoint_default(self, mocker): mocker.patch.dict( @@ -629,23 +728,31 @@ def test_configure_env_headers_otel_headers_endpoint_default(self, mocker): "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_HEADERS": "foo=bar,baz=qux", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.collector.na-01.cloud.solarwinds.com:443", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "foo=bar,baz=qux" - def test_configure_env_headers_otel_headers_endpoint_resolved(self, mocker): + def test_configure_env_headers_otel_headers_endpoint_resolved( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_HEADERS": "foo=bar,baz=qux", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.collector.eu-01.st-ssp.solarwinds.com:443", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + ) assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "foo=bar,baz=qux" def test_configure_env_headers_otel_headers_endpoint_non_swo(self, mocker): @@ -655,10 +762,12 @@ def test_configure_env_headers_otel_headers_endpoint_non_swo(self, mocker): "SW_APM_SERVICE_KEY": "foo-token:bar", "OTEL_EXPORTER_OTLP_HEADERS": "foo=bar,baz=qux", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://my-collector/", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://my-collector/" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://my-collector/" + ) assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "foo=bar,baz=qux" def test_configure_env_headers_swo_collector_invalid(self, mocker): @@ -667,11 +776,17 @@ def test_configure_env_headers_swo_collector_invalid(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "https://not-valid-will-default-exporter/", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) def test_configure_env_headers_swo_collector_invalid_not_apm(self, mocker): mocker.patch.dict( @@ -679,11 +794,17 @@ def test_configure_env_headers_swo_collector_invalid_not_apm(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "https://otel.collector.eu-01.st-ssp.solarwinds.com:443", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) def test_configure_env_headers_swo_collector_valid_resolved(self, mocker): mocker.patch.dict( @@ -691,75 +812,109 @@ def test_configure_env_headers_swo_collector_valid_resolved(self, mocker): { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "apm.collector.eu-01.st-ssp.solarwinds.com", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) - def test_configure_env_headers_otel_headers_swo_collector_invalid(self, mocker): + def test_configure_env_headers_otel_headers_swo_collector_invalid( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "https://not-valid-will-default-exporter/", "OTEL_EXPORTER_OTLP_HEADERS": "foo=bar,baz=qux", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "foo=bar,baz=qux" - def test_configure_env_headers_otel_headers_swo_collector_invalid_not_apm(self, mocker): + def test_configure_env_headers_otel_headers_swo_collector_invalid_not_apm( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "https://otel.collector.eu-01.st-ssp.solarwinds.com:443", "OTEL_EXPORTER_OTLP_HEADERS": "foo=bar,baz=qux", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.na-01.cloud.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.na-01.cloud.solarwinds.com:443" + ) assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "foo=bar,baz=qux" - def test_configure_env_headers_otel_headers_swo_collector_valid_resolved(self, mocker): + def test_configure_env_headers_otel_headers_swo_collector_valid_resolved( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "apm.collector.eu-01.st-ssp.solarwinds.com", "OTEL_EXPORTER_OTLP_HEADERS": "foo=bar,baz=qux", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.eu-01.st-ssp.solarwinds.com:443" + ) assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "foo=bar,baz=qux" - def test_configure_env_headers_otel_endpoint_vs_swo_collector_both_valid(self, mocker): + def test_configure_env_headers_otel_endpoint_vs_swo_collector_both_valid( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "apm.collector.eu-01.st-ssp.solarwinds.com", "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.collector.jp-01.st-dev.solarwinds.com:443", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "https://otel.collector.jp-01.st-dev.solarwinds.com:443" - assert os.environ[OTEL_EXPORTER_OTLP_HEADERS] == "authorization=Bearer%20foo-token" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "https://otel.collector.jp-01.st-dev.solarwinds.com:443" + ) + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) - def test_configure_env_headers_otel_endpoint_non_swo_vs_swo_collector(self, mocker): + def test_configure_env_headers_otel_endpoint_non_swo_vs_swo_collector( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_SERVICE_KEY": "foo-token:bar", "SW_APM_COLLECTOR": "apm.collector.eu-01.st-ssp.solarwinds.com", "OTEL_EXPORTER_OTLP_ENDPOINT": "http://my-export-endpoint", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] == "http://my-export-endpoint" + assert ( + os.environ[OTEL_EXPORTER_OTLP_ENDPOINT] + == "http://my-export-endpoint" + ) assert os.environ.get(OTEL_EXPORTER_OTLP_HEADERS) is None def test_configure_env_metrics_default_histogram_aggregation(self, mocker): @@ -767,136 +922,106 @@ def test_configure_env_metrics_default_histogram_aggregation(self, mocker): os.environ, { "OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION": "foo", - } + }, ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION] == "foo" + assert ( + os.environ[ + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION + ] + == "foo" + ) def test_configure_env_propagators(self, mocker): - mocker.patch.dict(os.environ, {"OTEL_PROPAGATORS": "tracecontext,solarwinds_propagator,foobar"}) + mocker.patch.dict( + os.environ, + {"OTEL_PROPAGATORS": "tracecontext,solarwinds_propagator,foobar"}, + ) distro.SolarWindsDistro()._configure() - assert os.environ[OTEL_PROPAGATORS] == "tracecontext,solarwinds_propagator,foobar" + assert ( + os.environ[OTEL_PROPAGATORS] + == "tracecontext,solarwinds_propagator,foobar" + ) assert os.environ[OTEL_TRACES_EXPORTER] == "otlp" assert os.environ[OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION] == "false" assert os.environ[OTEL_PYTHON_LOG_CODE_ATTRIBUTES] == "true" - assert os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") == "http,database,gen_ai_latest_experimental" + assert ( + os.environ.get("OTEL_SEMCONV_STABILITY_OPT_IN") + == "http,database,gen_ai_latest_experimental" + ) def test_load_instrumentor_aws_lambda_not_lambda_env(self, mocker): mock_apm_config = mocker.patch( "solarwinds_apm.distro.SolarWindsApmConfig" ) mock_apm_config.configure_mock( - **{ - "calculate_is_lambda": mocker.Mock(return_value=False) - } + calculate_is_lambda=mocker.Mock(return_value=False) ) mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() - mock_entry_point.configure_mock( - **{ - "load": mock_load, - "name": "aws-lambda", - } + mock_entry_point.configure_mock(load=mock_load, name="aws-lambda") + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) - mock_instrument.assert_called_once_with( - **{ - "foo": "bar", - } - ) + mock_instrument.assert_called_once_with(foo="bar") def test_load_instrumentor_aws_lambda_lambda_env(self, mocker): mock_apm_config = mocker.patch( "solarwinds_apm.distro.SolarWindsApmConfig" ) mock_apm_config.configure_mock( - **{ - "calculate_is_lambda": mocker.Mock(return_value=True) - } + calculate_is_lambda=mocker.Mock(return_value=True) ) mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() - mock_entry_point.configure_mock( - **{ - "load": mock_load, - "name": "aws-lambda", - } + mock_entry_point.configure_mock(load=mock_load, name="aws-lambda") + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) mock_instrument.assert_not_called() def test_load_instrumentor_no_commenting_configured(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() - mock_entry_point.configure_mock( - **{ - "load": mock_load - } + mock_entry_point.configure_mock(load=mock_load) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) - mock_instrument.assert_called_once_with( - **{ - "foo": "bar", - } - ) + mock_instrument.assert_called_once_with(foo="bar") def test_load_instrumentor_enable_commenting_not_on_list(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() - mock_entry_point.configure_mock( - **{ - "name": "not-on-list", - "load": mock_load, - } - ) + mock_entry_point.configure_mock(name="not-on-list", load=mock_load) mocker.patch( - "solarwinds_apm.distro._SQLCOMMENTERS", - [ - "this-is-on-the-list" - ] - ) + "solarwinds_apm.distro._SQLCOMMENTERS", ["this-is-on-the-list"] + ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.get_enable_commenter_env_map", return_value={ @@ -904,13 +1029,15 @@ def test_load_instrumentor_enable_commenting_not_on_list(self, mocker): "enable_commenter": True, "enable_attribute_commenter": False, } - } + }, ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.detect_commenter_options", - return_value="foo-options" + return_value="foo-options", + ) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) # Commenting not enabled because not on list mock_instrument.assert_called_once_with( foo="bar", @@ -920,27 +1047,17 @@ def test_load_instrumentor_enable_commenting_false(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() mock_entry_point.configure_mock( - **{ - "name": "foo-instrumentor", - "load": mock_load, - } + name="foo-instrumentor", load=mock_load ) mocker.patch( - "solarwinds_apm.distro._SQLCOMMENTERS", - [ - "foo-instrumentor" - ] - ) + "solarwinds_apm.distro._SQLCOMMENTERS", ["foo-instrumentor"] + ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.get_enable_commenter_env_map", return_value={ @@ -948,13 +1065,15 @@ def test_load_instrumentor_enable_commenting_false(self, mocker): "enable_commenter": False, "enable_attribute_commenter": True, } - } + }, ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.detect_commenter_options", - return_value="foo-options" + return_value="foo-options", + ) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) mock_instrument.assert_called_once_with( foo="bar", # If passed without enable_commenter=True, this does nothing @@ -965,27 +1084,17 @@ def test_load_instrumentor_enable_commenting_true(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() mock_entry_point.configure_mock( - **{ - "name": "foo-instrumentor", - "load": mock_load, - } + name="foo-instrumentor", load=mock_load ) mocker.patch( - "solarwinds_apm.distro._SQLCOMMENTERS", - [ - "foo-instrumentor" - ] - ) + "solarwinds_apm.distro._SQLCOMMENTERS", ["foo-instrumentor"] + ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.get_enable_commenter_env_map", return_value={ @@ -993,13 +1102,15 @@ def test_load_instrumentor_enable_commenting_true(self, mocker): "enable_commenter": True, "enable_attribute_commenter": False, } - } + }, ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.detect_commenter_options", - return_value="foo-options" + return_value="foo-options", + ) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) mock_instrument.assert_called_once_with( commenter_options="foo-options", enable_commenter=True, @@ -1010,27 +1121,17 @@ def test_load_instrumentor_enable_commenting_not_django(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() mock_entry_point.configure_mock( - **{ - "name": "foo-instrumentor", - "load": mock_load, - } + name="foo-instrumentor", load=mock_load ) mocker.patch( - "solarwinds_apm.distro._SQLCOMMENTERS", - [ - "foo-instrumentor" - ] - ) + "solarwinds_apm.distro._SQLCOMMENTERS", ["foo-instrumentor"] + ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.get_enable_commenter_env_map", return_value={ @@ -1038,13 +1139,15 @@ def test_load_instrumentor_enable_commenting_not_django(self, mocker): "enable_commenter": True, "enable_attribute_commenter": False, } - } + }, ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.detect_commenter_options", - return_value="foo-options" + return_value="foo-options", + ) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) mock_instrument.assert_called_once_with( commenter_options="foo-options", enable_commenter=True, @@ -1055,27 +1158,13 @@ def test_load_instrumentor_enable_commenting_django(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() - mock_entry_point.configure_mock( - **{ - "name": "django", - "load": mock_load, - } - ) - mocker.patch( - "solarwinds_apm.distro._SQLCOMMENTERS", - [ - "django" - ] - ) + mock_entry_point.configure_mock(name="django", load=mock_load) + mocker.patch("solarwinds_apm.distro._SQLCOMMENTERS", ["django"]) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.get_enable_commenter_env_map", return_value={ @@ -1083,13 +1172,15 @@ def test_load_instrumentor_enable_commenting_django(self, mocker): "enable_commenter": True, "enable_attribute_commenter": False, } - } + }, ) mocker.patch( "solarwinds_apm.distro.SolarWindsDistro.detect_commenter_options", - return_value="foo-options" + return_value="foo-options", + ) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) # No commenter_options because Django reads settings.py instead mock_instrument.assert_called_once_with( is_sql_commentor_enabled=True, @@ -1104,22 +1195,17 @@ def test_load_instrumentor_metrics_enabled(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() mock_entry_point.configure_mock( - **{ - "name": "foo-instrumentor", - "load": mock_load, - } + name="foo-instrumentor", load=mock_load + ) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) # No custom meter_provider set mock_instrument.assert_called_once_with( foo="bar", @@ -1127,8 +1213,7 @@ def test_load_instrumentor_metrics_enabled(self, mocker): def test_load_instrumentor_metrics_disabled(self, mocker): mocker.patch( - "solarwinds_apm.distro.NoOpMeterProvider", - return_value="noop" + "solarwinds_apm.distro.NoOpMeterProvider", return_value="noop" ) mocker.patch( "solarwinds_apm.distro.SolarWindsApmConfig.calculate_metrics_enabled", @@ -1137,22 +1222,17 @@ def test_load_instrumentor_metrics_disabled(self, mocker): mock_instrument = mocker.Mock() mock_instrumentor = mocker.Mock() mock_instrumentor.configure_mock( - return_value=mocker.Mock( - **{ - "instrument": mock_instrument - } - ) + return_value=mocker.Mock(instrument=mock_instrument) ) mock_load = mocker.Mock() mock_load.configure_mock(return_value=mock_instrumentor) mock_entry_point = mocker.Mock() mock_entry_point.configure_mock( - **{ - "name": "foo-instrumentor", - "load": mock_load, - } + name="foo-instrumentor", load=mock_load + ) + distro.SolarWindsDistro().load_instrumentor( + mock_entry_point, foo="bar" ) - distro.SolarWindsDistro().load_instrumentor(mock_entry_point, **{"foo": "bar"}) # passed custom meter_provider as no-op mock_instrument.assert_called_once_with( foo="bar", @@ -1190,7 +1270,7 @@ def test_get_enable_commenter_env_map_invalid_just_a_comma(self, mocker): { "SW_APM_ENABLED_SQLCOMMENT": ",", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": ",", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1215,13 +1295,15 @@ def test_get_enable_commenter_env_map_invalid_just_a_comma(self, mocker): }, } - def test_get_enable_commenter_env_map_invalid_missing_equals_sign_single_val(self, mocker): + def test_get_enable_commenter_env_map_invalid_missing_equals_sign_single_val( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_ENABLED_SQLCOMMENT": "django", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "django", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1246,13 +1328,15 @@ def test_get_enable_commenter_env_map_invalid_missing_equals_sign_single_val(sel }, } - def test_get_enable_commenter_env_map_invalid_missing_equals_sign_multiple_first(self, mocker): + def test_get_enable_commenter_env_map_invalid_missing_equals_sign_multiple_first( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_ENABLED_SQLCOMMENT": "django,flask=true", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "django,flask=false", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1277,13 +1361,15 @@ def test_get_enable_commenter_env_map_invalid_missing_equals_sign_multiple_first }, } - def test_get_enable_commenter_env_map_invalid_missing_equals_sign_multiple_last(self, mocker): + def test_get_enable_commenter_env_map_invalid_missing_equals_sign_multiple_last( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_ENABLED_SQLCOMMENT": "flask=true,django", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "flask=false,django", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1314,7 +1400,7 @@ def test_get_enable_commenter_env_map_valid_ignored_values(self, mocker): { "SW_APM_ENABLED_SQLCOMMENT": "django=true,flask=foobar,psycopg=123", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "django=false,flask=foobar,psycopg=123", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1345,7 +1431,7 @@ def test_get_enable_commenter_env_map_valid_mixed_case(self, mocker): { "SW_APM_ENABLED_SQLCOMMENT": "dJAnGO=tRuE,FlaSK=TrUe", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "dJAnGO=fAlSe,FlaSK=FaLsE", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1370,13 +1456,15 @@ def test_get_enable_commenter_env_map_valid_mixed_case(self, mocker): }, } - def test_get_enable_commenter_env_map_valid_whitespace_stripped(self, mocker): + def test_get_enable_commenter_env_map_valid_whitespace_stripped( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_ENABLED_SQLCOMMENT": "django = true , flask= true ", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "django = false , flask= false ", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1407,7 +1495,7 @@ def test_get_enable_commenter_env_map_valid_update_existing(self, mocker): { "SW_APM_ENABLED_SQLCOMMENT": "django=true,flask=true,psycopg=true,psycopg2=true,sqlalchemy=true", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "django=false,flask=false,psycopg=false,psycopg2=false,sqlalchemy=false", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1432,13 +1520,15 @@ def test_get_enable_commenter_env_map_valid_update_existing(self, mocker): }, } - def test_get_enable_commenter_env_map_valid_ignores_if_not_on_list(self, mocker): + def test_get_enable_commenter_env_map_valid_ignores_if_not_on_list( + self, mocker + ): mocker.patch.dict( os.environ, { "SW_APM_ENABLED_SQLCOMMENT": "flask=true,foobar=true", "SW_APM_ENABLED_SQLCOMMENT_ATTRIBUTE": "flask=false,foobar=false", - } + }, ) assert distro.SolarWindsDistro().get_enable_commenter_env_map() == { "django": { @@ -1469,19 +1559,28 @@ def test_detect_commenter_options_not_set(self, mocker): assert result == {} def test_detect_commenter_options_strip_mixed(self, mocker): - mocker.patch.dict(os.environ, {"SW_APM_OPTIONS_SQLCOMMENT": "invalid-kv, foofoo=TrUe ,barbar = faLSE, bazbaz=qux "}) + mocker.patch.dict( + os.environ, + { + "SW_APM_OPTIONS_SQLCOMMENT": "invalid-kv, foofoo=TrUe ,barbar = faLSE, bazbaz=qux " + }, + ) result = distro.SolarWindsDistro().detect_commenter_options() - assert result.get("foofoo") == True - assert result.get("barbar") == False + assert result.get("foofoo") + assert not result.get("barbar") assert result.get("bazbaz") is None def test_detect_commenter_options_invalid_kv_ignored(self, mocker): - mocker.patch.dict(os.environ, {"SW_APM_OPTIONS_SQLCOMMENT": "invalid-kv,foo=bar"}) + mocker.patch.dict( + os.environ, {"SW_APM_OPTIONS_SQLCOMMENT": "invalid-kv,foo=bar"} + ) result = distro.SolarWindsDistro().detect_commenter_options() assert result == {} def test_detect_commenter_options_valid_kvs(self, mocker): - mocker.patch.dict(os.environ, {"SW_APM_OPTIONS_SQLCOMMENT": "foo=true,bar=FaLSe"}) + mocker.patch.dict( + os.environ, {"SW_APM_OPTIONS_SQLCOMMENT": "foo=true,bar=FaLSe"} + ) result = distro.SolarWindsDistro().detect_commenter_options() assert result == { "foo": True, @@ -1493,19 +1592,27 @@ def test_detect_commenter_options_strip_whitespace_ok(self, mocker): os.environ, { "SW_APM_OPTIONS_SQLCOMMENT": " foo = tRUe , bar = falsE " - } + }, ) result = distro.SolarWindsDistro().detect_commenter_options() - assert result.get("foo") == True - assert result.get("bar") == False + assert result.get("foo") + assert not result.get("bar") def test_detect_commenter_options_strip_mix(self, mocker): - mocker.patch.dict(os.environ, {"SW_APM_OPTIONS_SQLCOMMENT": "invalid-kv, foo=TrUe ,bar = faLSE, baz=qux "}) + mocker.patch.dict( + os.environ, + { + "SW_APM_OPTIONS_SQLCOMMENT": "invalid-kv, foo=TrUe ,bar = faLSE, baz=qux " + }, + ) result = distro.SolarWindsDistro().detect_commenter_options() - assert result.get("foo") == True - assert result.get("bar") == False + assert result.get("foo") + assert not result.get("bar") assert result.get("baz") is None def test_get_semconv_opt_in(self): # TODO: Support other signal types when available - assert distro.SolarWindsDistro().get_semconv_opt_in() == "http,database,gen_ai_latest_experimental" \ No newline at end of file + assert ( + distro.SolarWindsDistro().get_semconv_opt_in() + == "http,database,gen_ai_latest_experimental" + ) diff --git a/tests/unit/test_k8s.py b/tests/unit/test_k8s.py index 2fcaaa63b..1baf3644b 100644 --- a/tests/unit/test_k8s.py +++ b/tests/unit/test_k8s.py @@ -6,28 +6,32 @@ import os -import tempfile -import uuid import random import string +import tempfile +import uuid from contextlib import suppress import pytest - from opentelemetry.semconv.resource import ResourceAttributes from solarwinds_apm.k8s import K8sResourceDetector -NAMESPACE_FILE = os.path.join(tempfile.gettempdir(), "solarwinds-apm-k8s-namespace") -MOUNTINFO_FILE = os.path.join(tempfile.gettempdir(), "solarwinds-apm-mountinfo") +NAMESPACE_FILE = os.path.join( + tempfile.gettempdir(), "solarwinds-apm-k8s-namespace" +) +MOUNTINFO_FILE = os.path.join( + tempfile.gettempdir(), "solarwinds-apm-mountinfo" +) -ENV_NAMESPACE = ''.join(random.choices(string.hexdigits, k=16)) -FILE_NAMESPACE = ''.join(random.choices(string.hexdigits, k=16)) +ENV_NAMESPACE = "".join(random.choices(string.hexdigits, k=16)) +FILE_NAMESPACE = "".join(random.choices(string.hexdigits, k=16)) ENV_UID = str(uuid.uuid4()) FILE_UID = str(uuid.uuid4()) -ENV_NAME = ''.join(random.choices(string.hexdigits, k=8)) +ENV_NAME = "".join(random.choices(string.hexdigits, k=8)) + @pytest.fixture(autouse=True) def cleanup(): @@ -36,10 +40,12 @@ def cleanup(): os.remove(NAMESPACE_FILE) os.remove(MOUNTINFO_FILE) + def file_namespace(): with open(NAMESPACE_FILE, "w") as f: f.write(f"{FILE_NAMESPACE}\n") + def file_uid(): with open(MOUNTINFO_FILE, "w") as f: f.write(f""" @@ -71,12 +77,16 @@ def file_uid(): 628 765 0:151 / /sys/firmware ro,relatime - tmpfs tmpfs ro,context="system_u:object_r:data_t:s0:c171,c852" """) + def test_detects_attributes_from_env(mocker): - mocker.patch.dict(os.environ, { - 'SW_K8S_POD_NAMESPACE': ENV_NAMESPACE, - 'SW_K8S_POD_UID': ENV_UID, - 'SW_K8S_POD_NAME': ENV_NAME, - }) + mocker.patch.dict( + os.environ, + { + "SW_K8S_POD_NAMESPACE": ENV_NAMESPACE, + "SW_K8S_POD_UID": ENV_UID, + "SW_K8S_POD_NAME": ENV_NAME, + }, + ) k8s_detector = K8sResourceDetector(NAMESPACE_FILE, MOUNTINFO_FILE) resource = k8s_detector.detect() @@ -87,6 +97,7 @@ def test_detects_attributes_from_env(mocker): ResourceAttributes.K8S_POD_NAME: ENV_NAME, } + def test_detects_attributes_from_files(): file_namespace() file_uid() @@ -103,12 +114,16 @@ def test_detects_attributes_from_files(): assert resource.attributes == expected_attributes + def test_prefers_env_over_files(mocker): - mocker.patch.dict(os.environ, { - 'SW_K8S_POD_NAMESPACE': ENV_NAMESPACE, - 'SW_K8S_POD_UID': ENV_UID, - 'SW_K8S_POD_NAME': ENV_NAME, - }) + mocker.patch.dict( + os.environ, + { + "SW_K8S_POD_NAMESPACE": ENV_NAMESPACE, + "SW_K8S_POD_UID": ENV_UID, + "SW_K8S_POD_NAME": ENV_NAME, + }, + ) file_namespace() file_uid() @@ -121,11 +136,15 @@ def test_prefers_env_over_files(mocker): ResourceAttributes.K8S_POD_NAME: ENV_NAME, } + def test_doesnt_detect_uid_or_name_without_namespace(mocker): - mocker.patch.dict(os.environ, { - 'SW_K8S_POD_UID': ENV_UID, - 'SW_K8S_POD_NAME': ENV_NAME, - }) + mocker.patch.dict( + os.environ, + { + "SW_K8S_POD_UID": ENV_UID, + "SW_K8S_POD_NAME": ENV_NAME, + }, + ) file_uid() k8s_detector = K8sResourceDetector(NAMESPACE_FILE, MOUNTINFO_FILE) diff --git a/tests/unit/test_oboe/test_dice.py b/tests/unit/test_oboe/test_dice.py index 2e2a7cdc8..cb874a3e9 100644 --- a/tests/unit/test_oboe/test_dice.py +++ b/tests/unit/test_oboe/test_dice.py @@ -48,8 +48,8 @@ def test_rate_setter_negative_value(dice): def test_roll_zero_rate(dice_zero_rate): - assert all(dice_zero_rate.roll() == False for _ in range(1000)) + assert all(not dice_zero_rate.roll() for _ in range(1000)) def test_roll_full_rate(dice_full_rate): - assert all(dice_full_rate.roll() == True for _ in range(1000)) + assert all(dice_full_rate.roll() for _ in range(1000)) diff --git a/tests/unit/test_oboe/test_http_sampler.py b/tests/unit/test_oboe/test_http_sampler.py index fe78852e0..236f45460 100644 --- a/tests/unit/test_oboe/test_http_sampler.py +++ b/tests/unit/test_oboe/test_http_sampler.py @@ -6,40 +6,43 @@ import json import os import socket -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from opentelemetry import trace -from opentelemetry.sdk.metrics import MeterProvider, AlwaysOnExemplarFilter +from opentelemetry.sdk.metrics import AlwaysOnExemplarFilter, MeterProvider from opentelemetry.sdk.metrics._internal.export import InMemoryMetricReader from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) from solarwinds_apm.oboe.configuration import Configuration -from solarwinds_apm.oboe.http_sampler import HttpSampler, DAEMON_THREAD_JOIN_TIMEOUT +from solarwinds_apm.oboe.http_sampler import ( + DAEMON_THREAD_JOIN_TIMEOUT, + HttpSampler, +) def test_valid_service_key_samples_created_spans(): # This test requires a valid service key to be set in the environment service_key = os.getenv("SW_APM_SERVICE_KEY") if service_key: - l = service_key.split(":") - if len(l) == 2: - bearer = l[0] - service = l[1] + key_parts = service_key.split(":") + if len(key_parts) == 2: + bearer = key_parts[0] + service = key_parts[1] meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = HttpSampler( meter_provider=meter_provider, config=Configuration( collector="https://apm.collector.na-01.cloud.solarwinds.com", service=service, - headers={ - "Authorization": f"Bearer {bearer}" - }, + headers={"Authorization": f"Bearer {bearer}"}, enabled=True, trigger_trace_enabled=True, tracing_mode=None, @@ -50,7 +53,11 @@ def test_valid_service_key_samples_created_spans(): ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor( + span_exporter=memory_exporter + ) + ) tracer = trace.get_tracer("test", tracer_provider=tracer_provider) sampler.wait_until_ready(1) with tracer.start_as_current_span("test") as span: @@ -66,16 +73,14 @@ def test_valid_service_key_samples_created_spans(): def test_invalid_service_key_does_not_sample_created_spans(): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = HttpSampler( meter_provider=meter_provider, config=Configuration( collector="https://apm.collector.na-01.cloud.solarwinds.com", service="apm-python-test", - headers={ - "Authorization": "Bearer oh-no" - }, + headers={"Authorization": "Bearer oh-no"}, enabled=True, trigger_trace_enabled=True, tracing_mode=None, @@ -86,7 +91,9 @@ def test_invalid_service_key_does_not_sample_created_spans(): ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) tracer = trace.get_tracer("test", tracer_provider=tracer_provider) sampler.wait_until_ready(1) with tracer.start_as_current_span("test") as span: @@ -98,15 +105,14 @@ def test_invalid_service_key_does_not_sample_created_spans(): def test_invalid_collector_does_not_sample_created_spans(): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = HttpSampler( meter_provider=meter_provider, config=Configuration( collector="https://collector.invalid", service="apm-python-test", - headers={ - }, + headers={}, enabled=True, trigger_trace_enabled=True, tracing_mode=None, @@ -117,7 +123,9 @@ def test_invalid_collector_does_not_sample_created_spans(): ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) tracer = trace.get_tracer("test", tracer_provider=tracer_provider) sampler.wait_until_ready(1) with tracer.start_as_current_span("test") as span: @@ -145,7 +153,7 @@ def meter_provider(): return MeterProvider() -@patch('requests.get') +@patch("requests.get") def test_fetch_from_collector_success(mock_get, config, meter_provider): mock_response = MagicMock() mock_response.json.return_value = { @@ -160,12 +168,14 @@ def test_fetch_from_collector_success(mock_get, config, meter_provider): "TriggerRelaxedBucketRate": 1, "TriggerStrictBucketCapacity": 6, "TriggerStrictBucketRate": 0.1, - "SignatureKey": "signature" - } + "SignatureKey": "signature", + }, } mock_response.status_code = 200 mock_get.return_value = mock_response - sampler = HttpSampler(meter_provider=meter_provider, config=config, initial=None) + sampler = HttpSampler( + meter_provider=meter_provider, config=config, initial=None + ) result = sampler._fetch_from_collector() assert result == { "value": 1000000, @@ -179,13 +189,14 @@ def test_fetch_from_collector_success(mock_get, config, meter_provider): "TriggerRelaxedBucketRate": 1, "TriggerStrictBucketCapacity": 6, "TriggerStrictBucketRate": 0.1, - "SignatureKey": "signature" - } + "SignatureKey": "signature", + }, } mock_get.assert_called_with( f"https://apm.collector.na-01.cloud.solarwinds.com/v1/settings/test_service/{socket.gethostname()}", headers={"Authorization": "Bearer test_token"}, - timeout=10) + timeout=10, + ) # one in constructor and one in test case assert mock_get.call_count == 2 @@ -206,7 +217,9 @@ def test_fetch_from_collector_invalid_json_returns_empty_dict_and_thread_survive mock_response.json.side_effect = json_error mock_get.return_value = mock_response - sampler = HttpSampler(meter_provider=meter_provider, config=config, initial=None) + sampler = HttpSampler( + meter_provider=meter_provider, config=config, initial=None + ) try: result = sampler._fetch_from_collector() assert result == {} @@ -219,7 +232,9 @@ def test_fetch_from_collector_invalid_json_returns_empty_dict_and_thread_survive def test_shutdown(config, meter_provider): - sampler = HttpSampler(meter_provider=meter_provider, config=config, initial=None) + sampler = HttpSampler( + meter_provider=meter_provider, config=config, initial=None + ) sampler.shutdown() assert sampler._shutdown_event.is_set() sampler._daemon_thread.join(timeout=DAEMON_THREAD_JOIN_TIMEOUT) diff --git a/tests/unit/test_oboe/test_json_sampler.py b/tests/unit/test_oboe/test_json_sampler.py index 741809b7d..dd80ede9d 100644 --- a/tests/unit/test_oboe/test_json_sampler.py +++ b/tests/unit/test_oboe/test_json_sampler.py @@ -4,6 +4,7 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +import contextlib import json import os import tempfile @@ -15,7 +16,9 @@ from opentelemetry.sdk.metrics._internal.export import InMemoryMetricReader from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) from solarwinds_apm.oboe.configuration import Configuration from solarwinds_apm.oboe.json_sampler import JsonSampler @@ -27,48 +30,63 @@ def json_sampler_tracer_memory_exporter(): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = JsonSampler( meter_provider=meter_provider, - config=Configuration(enabled=True, service="test", collector="", headers={}, - tracing_mode=True, - trigger_trace_enabled=True, transaction_name=None, - transaction_settings=[]), - path=PATH + config=Configuration( + enabled=True, + service="test", + collector="", + headers={}, + tracing_mode=True, + trigger_trace_enabled=True, + transaction_name=None, + transaction_settings=[], + ), + path=PATH, ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) tracer = trace.get_tracer("test", tracer_provider=tracer_provider) return tracer, memory_exporter def test_valid_file_samples_created_spans(json_sampler_tracer_memory_exporter): with open(PATH, "w") as f: - json.dump([{ - "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE,OVERRIDE", - "value": 1_000_000, - "arguments": { - "BucketCapacity": 100, - "BucketRate": 10, - }, - "timestamp": int(time.time()), - "ttl": 60, - }], f) + json.dump( + [ + { + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE,OVERRIDE", + "value": 1_000_000, + "arguments": { + "BucketCapacity": 100, + "BucketRate": 10, + }, + "timestamp": int(time.time()), + "ttl": 60, + } + ], + f, + ) tracer, memory_exporter = json_sampler_tracer_memory_exporter with tracer.start_as_current_span("test") as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() assert len(spans) == 1 - assert 'SampleRate' in spans[0].attributes - assert 'SampleSource' in spans[0].attributes - assert 'BucketCapacity' in spans[0].attributes - assert 'BucketRate' in spans[0].attributes + assert "SampleRate" in spans[0].attributes + assert "SampleSource" in spans[0].attributes + assert "BucketCapacity" in spans[0].attributes + assert "BucketRate" in spans[0].attributes os.remove(PATH) -def test_invalid_file_no_samples_created_spans(json_sampler_tracer_memory_exporter): +def test_invalid_file_no_samples_created_spans( + json_sampler_tracer_memory_exporter, +): with open(PATH, "w") as f: json.dump({"hello": "world"}, f) tracer, memory_exporter = json_sampler_tracer_memory_exporter @@ -79,12 +97,11 @@ def test_invalid_file_no_samples_created_spans(json_sampler_tracer_memory_export os.remove(PATH) -def test_missing_file_no_samples_created_spans(json_sampler_tracer_memory_exporter): - try: +def test_missing_file_no_samples_created_spans( + json_sampler_tracer_memory_exporter, +): + with contextlib.suppress(FileNotFoundError): os.remove(PATH) - except FileNotFoundError: - # It's okay if the file does not exist - pass tracer, memory_exporter = json_sampler_tracer_memory_exporter with tracer.start_as_current_span("test") as span: assert not span.is_recording() @@ -92,18 +109,25 @@ def test_missing_file_no_samples_created_spans(json_sampler_tracer_memory_export assert len(spans) == 0 -def test_expired_file_no_samples_created_spans(json_sampler_tracer_memory_exporter): +def test_expired_file_no_samples_created_spans( + json_sampler_tracer_memory_exporter, +): with open(PATH, "w") as f: - json.dump([{ - "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE,OVERRIDE", - "value": 1_000_000, - "arguments": { - "BucketCapacity": 100, - "BucketRate": 10, - }, - "timestamp": int(time.time()) - 120, - "ttl": 60, - }], f) + json.dump( + [ + { + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE,OVERRIDE", + "value": 1_000_000, + "arguments": { + "BucketCapacity": 100, + "BucketRate": 10, + }, + "timestamp": int(time.time()) - 120, + "ttl": 60, + } + ], + f, + ) tracer, memory_exporter = json_sampler_tracer_memory_exporter with tracer.start_as_current_span("test") as span: assert not span.is_recording() @@ -112,24 +136,31 @@ def test_expired_file_no_samples_created_spans(json_sampler_tracer_memory_export os.remove(PATH) -def test_samples_after_reading_new_settings(json_sampler_tracer_memory_exporter): +def test_samples_after_reading_new_settings( + json_sampler_tracer_memory_exporter, +): with open(PATH, "w") as f: - json.dump([{ - "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE,OVERRIDE", - "value": 1_000_000, - "arguments": { - "BucketCapacity": 100, - "BucketRate": 10, - }, - "timestamp": int(time.time()), - "ttl": 60, - }], f) + json.dump( + [ + { + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE,OVERRIDE", + "value": 1_000_000, + "arguments": { + "BucketCapacity": 100, + "BucketRate": 10, + }, + "timestamp": int(time.time()), + "ttl": 60, + } + ], + f, + ) tracer, memory_exporter = json_sampler_tracer_memory_exporter with tracer.start_as_current_span("test") as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() assert len(spans) == 1 - assert 'SampleRate' in spans[0].attributes - assert 'SampleSource' in spans[0].attributes - assert 'BucketCapacity' in spans[0].attributes - assert 'BucketRate' in spans[0].attributes + assert "SampleRate" in spans[0].attributes + assert "SampleSource" in spans[0].attributes + assert "BucketCapacity" in spans[0].attributes + assert "BucketRate" in spans[0].attributes diff --git a/tests/unit/test_oboe/test_oboe_sampler.py b/tests/unit/test_oboe/test_oboe_sampler.py index 93de268fb..e2f2b7345 100644 --- a/tests/unit/test_oboe/test_oboe_sampler.py +++ b/tests/unit/test_oboe/test_oboe_sampler.py @@ -7,31 +7,55 @@ import hashlib import hmac - import os import time from collections.abc import Sequence - import pytest from opentelemetry import trace -from opentelemetry.sdk.metrics import MeterProvider, AlwaysOnExemplarFilter +from opentelemetry.context import Context +from opentelemetry.sdk.metrics import AlwaysOnExemplarFilter, MeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.resources import Attributes from opentelemetry.sdk.trace import RandomIdGenerator -from opentelemetry.trace import SpanKind, Link, TraceState, TraceFlags, get_current_span +from opentelemetry.trace import ( + Link, + SpanKind, + TraceFlags, + TraceState, + get_current_span, +) from typing_extensions import override from solarwinds_apm.apm_constants import INTL_SWO_X_OPTIONS_RESPONSE_KEY -from solarwinds_apm.oboe.oboe_sampler import OboeSampler, SW_KEYS_ATTRIBUTE, \ - BUCKET_RATE_ATTRIBUTE, BUCKET_CAPACITY_ATTRIBUTE, SAMPLE_RATE_ATTRIBUTE, SAMPLE_SOURCE_ATTRIBUTE, TRACESTATE_CAPTURE_ATTRIBUTE -from solarwinds_apm.oboe.settings import LocalSettings, Settings, SampleSource, Flags, BucketType, BucketSettings +from solarwinds_apm.oboe.oboe_sampler import ( + BUCKET_CAPACITY_ATTRIBUTE, + BUCKET_RATE_ATTRIBUTE, + SAMPLE_RATE_ATTRIBUTE, + SAMPLE_SOURCE_ATTRIBUTE, + SW_KEYS_ATTRIBUTE, + TRACESTATE_CAPTURE_ATTRIBUTE, + OboeSampler, +) +from solarwinds_apm.oboe.settings import ( + BucketSettings, + BucketType, + Flags, + LocalSettings, + SampleSource, + Settings, +) from solarwinds_apm.oboe.trace_options import RequestHeaders, ResponseHeaders class MakeRequestHeaders: - def __init__(self, trigger_trace: bool | None = None, kvs: dict | None = None, signature=None, - signature_key: str | None = None): + def __init__( + self, + trigger_trace: bool | None = None, + kvs: dict | None = None, + signature=None, + signature_key: str | None = None, + ): self._trigger_trace = trigger_trace self._kvs = kvs self._signature = signature @@ -55,20 +79,36 @@ def signature_key(self) -> str | None: def make_request_headers(options=MakeRequestHeaders()) -> RequestHeaders: - if options.trigger_trace is None and options.kvs is None and options.signature is None: - return RequestHeaders(x_trace_options=None, x_trace_options_signature=None) + if ( + options.trigger_trace is None + and options.kvs is None + and options.signature is None + ): + return RequestHeaders( + x_trace_options=None, x_trace_options_signature=None + ) timestamp = int(time.time()) - if isinstance(options.signature_key, str) and options.signature == "bad-timestamp": + if ( + isinstance(options.signature_key, str) + and options.signature == "bad-timestamp" + ): timestamp -= 10 * 60 ts = f"ts={timestamp}" trigger_trace = "trigger-trace" if options.trigger_trace else None kvs = [f"{k}={v}" for k, v in options.kvs.items() if options.kvs] - headers = RequestHeaders(x_trace_options=";".join(filter(None, [trigger_trace, *kvs, ts])), - x_trace_options_signature=None) + headers = RequestHeaders( + x_trace_options=";".join(filter(None, [trigger_trace, *kvs, ts])), + x_trace_options_signature=None, + ) if options.signature: - key = options.signature_key if options.signature_key else os.urandom(8).hex() - headers.x_trace_options_signature = hmac.new(str.encode(key), str.encode(headers.x_trace_options), - hashlib.sha1).hexdigest() + key = ( + options.signature_key + if options.signature_key + else os.urandom(8).hex() + ) + headers.x_trace_options_signature = hmac.new( + str.encode(key), str.encode(headers.x_trace_options), hashlib.sha1 + ).hexdigest() return headers @@ -83,7 +123,9 @@ def check_counters(sampler, counter_names): assert len(metrics_data.resource_metrics) == 1 scope_metrics = metrics_data.resource_metrics[0].scope_metrics sampling_scope_metrics = [ - sm for sm in scope_metrics if sm.scope.name == "sw.apm.sampling.metrics" + sm + for sm in scope_metrics + if sm.scope.name == "sw.apm.sampling.metrics" ] assert len(sampling_scope_metrics) == 1 scope_metrics_data = sampling_scope_metrics[0].metrics @@ -97,8 +139,12 @@ def check_counters(sampler, counter_names): class MockSamplerOptions: - def __init__(self, settings: Settings | None = None, local_settings: LocalSettings | None = None, - request_headers: RequestHeaders | None = None): + def __init__( + self, + settings: Settings | None = None, + local_settings: LocalSettings | None = None, + request_headers: RequestHeaders | None = None, + ): self._settings = settings self._local_settings = local_settings self._request_headers = request_headers @@ -121,7 +167,7 @@ def __init__(self, options: MockSamplerOptions): self._metric_reader = InMemoryMetricReader() meter_provider = MeterProvider( metric_readers=[self._metric_reader], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) super().__init__(meter_provider=meter_provider) self._local_settings = options.local_settings @@ -130,70 +176,120 @@ def __init__(self, options: MockSamplerOptions): self.update_settings(options.settings) self._response_headers = None - def _create_parent(self, trace_flags: trace.TraceFlags, is_remote=False, sw=None, other_trace_state=False, xtrace_options_response=False) -> Context | None: + def _create_parent( + self, + trace_flags: trace.TraceFlags, + is_remote=False, + sw=None, + other_trace_state=False, + xtrace_options_response=False, + ) -> Context | None: if trace_flags is None: return None - return trace.set_span_in_context(self._create_parent_span(trace_flags, is_remote, sw, other_trace_state, xtrace_options_response)) + return trace.set_span_in_context( + self._create_parent_span( + trace_flags, + is_remote, + sw, + other_trace_state, + xtrace_options_response, + ) + ) @staticmethod - def _create_parent_span(trace_flags: trace.TraceFlags, is_remote=False, sw=None, other_trace_state=False, xtrace_options_response=False) -> trace.NonRecordingSpan: + def _create_parent_span( + trace_flags: trace.TraceFlags, + is_remote=False, + sw=None, + other_trace_state=False, + xtrace_options_response=False, + ) -> trace.NonRecordingSpan: generator = RandomIdGenerator() trace_id = generator.generate_trace_id() span_id = generator.generate_span_id() trace_state = None if isinstance(sw, str) and sw == "inverse": trace_state = TraceState( - [("sw", format(span_id, "016x") + "-0" + ("0" if trace_flags == TraceFlags.SAMPLED else "1"))]) + [ + ( + "sw", + format(span_id, "016x") + + "-0" + + ("0" if trace_flags == TraceFlags.SAMPLED else "1"), + ) + ] + ) elif isinstance(sw, bool): trace_state = TraceState( - [("sw", format(span_id, "016x") + "-0" + ("1" if trace_flags == TraceFlags.SAMPLED else "0"))]) + [ + ( + "sw", + format(span_id, "016x") + + "-0" + + ("1" if trace_flags == TraceFlags.SAMPLED else "0"), + ) + ] + ) if other_trace_state: if trace_state is None: trace_state = TraceState() - trace_state = trace_state.add("vendor1", "value1").add("vendor2", "value2") + trace_state = trace_state.add("vendor1", "value1").add( + "vendor2", "value2" + ) if xtrace_options_response: if trace_state is None: trace_state = TraceState() - trace_state = trace_state.add(INTL_SWO_X_OPTIONS_RESPONSE_KEY, "response") + trace_state = trace_state.add( + INTL_SWO_X_OPTIONS_RESPONSE_KEY, "response" + ) - span_context = trace.SpanContext(trace_id=trace_id, span_id=span_id, is_remote=is_remote, - trace_flags=trace_flags, trace_state=trace_state) + span_context = trace.SpanContext( + trace_id=trace_id, + span_id=span_id, + is_remote=is_remote, + trace_flags=trace_flags, + trace_state=trace_state, + ) return trace.NonRecordingSpan(span_context) @override - def local_settings(self, - parent_context: "Context" | None, - trace_id: int, - name: str, - kind: SpanKind | None = None, - attributes: Attributes = None, - links: Sequence["Link"] | None = None, - trace_state: "TraceState" | None = None) -> LocalSettings: + def local_settings( + self, + parent_context: Context | None, + trace_id: int, + name: str, + kind: SpanKind | None = None, + attributes: Attributes = None, + links: Sequence[Link] | None = None, + trace_state: TraceState | None = None, + ) -> LocalSettings: return self._local_settings @override - def request_headers(self, - parent_context: "Context" | None, - trace_id: int, - name: str, - kind: SpanKind | None = None, - attributes: Attributes = None, - links: Sequence["Link"] | None = None, - trace_state: "TraceState" | None = None - ) -> RequestHeaders: + def request_headers( + self, + parent_context: Context | None, + trace_id: int, + name: str, + kind: SpanKind | None = None, + attributes: Attributes = None, + links: Sequence[Link] | None = None, + trace_state: TraceState | None = None, + ) -> RequestHeaders: return self._request_headers @override - def set_response_headers(self, - headers: ResponseHeaders, - parent_context: "Context" | None, - trace_id: int, - name: str, - kind: SpanKind | None = None, - attributes: Attributes = None, - links: Sequence["Link"] | None = None, - trace_state: "TraceState" | None = None - ) -> "TraceState" | None: + def set_response_headers( + self, + headers: ResponseHeaders, + parent_context: Context | None, + trace_id: int, + name: str, + kind: SpanKind | None = None, + attributes: Attributes = None, + links: Sequence[Link] | None = None, + trace_state: TraceState | None = None, + ) -> TraceState | None: self._response_headers = headers return None @@ -206,291 +302,515 @@ def metric_reader(self): return self._metric_reader def __str__(self): - return f"Test Sampler" + return "Test Sampler" class TestInvalidXTraceOptionsSignature: def test_rejects_missing_signature_key(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=1_000_000, - sample_source=SampleSource.REMOTE, - flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, signature=True, kvs={"custom-key": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "rejects_missing_signature_key") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=1_000_000, + sample_source=SampleSource.REMOTE, + flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + signature=True, + kvs={"custom-key": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "rejects_missing_signature_key", + ) assert not sample.decision.is_sampled() assert not sample.decision.is_recording() assert sample.attributes == {} - assert "auth=no-signature-key" in sampler.response_headers.x_trace_options_response + assert ( + "auth=no-signature-key" + in sampler.response_headers.x_trace_options_response + ) check_counters(sampler, ["trace.service.request_count"]) def test_rejects_bad_timestamp(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=1_000_000, - sample_source=SampleSource.REMOTE, - flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key="key", - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, signature="bad-timestamp", signature_key="key", - kvs={"custom-key": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "rejects_bad_timestamp") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=1_000_000, + sample_source=SampleSource.REMOTE, + flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key="key", + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + signature="bad-timestamp", + signature_key="key", + kvs={"custom-key": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "rejects_bad_timestamp", + ) assert not sample.decision.is_sampled() assert not sample.decision.is_recording() assert sample.attributes == {} - assert "auth=bad-timestamp" in sampler.response_headers.x_trace_options_response + assert ( + "auth=bad-timestamp" + in sampler.response_headers.x_trace_options_response + ) check_counters(sampler, ["trace.service.request_count"]) def test_rejects_bad_signature(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=1_000_000, - sample_source=SampleSource.REMOTE, - flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key="key1", - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, signature=True, signature_key="key2", - kvs={"custom-key": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "rejects_bad_signature") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=1_000_000, + sample_source=SampleSource.REMOTE, + flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key="key1", + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + signature=True, + signature_key="key2", + kvs={"custom-key": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "rejects_bad_signature", + ) assert not sample.decision.is_sampled() assert not sample.decision.is_recording() assert sample.attributes == {} - assert "auth=bad-signature" in sampler.response_headers.x_trace_options_response + assert ( + "auth=bad-signature" + in sampler.response_headers.x_trace_options_response + ) check_counters(sampler, ["trace.service.request_count"]) class TestMissingSettings: def test_does_not_sample(self): - sampler = MockSampler(MockSamplerOptions( - settings=None, - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) + sampler = MockSampler( + MockSamplerOptions( + settings=None, + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) generator = RandomIdGenerator() - sample = sampler.should_sample(None, generator.generate_trace_id(), "does_not_sample") + sample = sampler.should_sample( + None, generator.generate_trace_id(), "does_not_sample" + ) assert not sample.decision.is_sampled() assert not sample.decision.is_recording() check_counters(sampler, ["trace.service.request_count"]) def test_expires_after_ttl(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key=None, - timestamp=int(time.time()) - 60, - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, "expires_after_ttl") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key=None, + timestamp=int(time.time()) - 60, + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "expires_after_ttl", + ) assert not sample.decision.is_sampled() assert not sample.decision.is_recording() check_counters(sampler, ["trace.service.request_count"]) def test_respects_x_trace_options_keys_and_values(self): - sampler = MockSampler(MockSamplerOptions( - settings=None, - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(kvs={"custom-key": "value", "sw-keys": "sw-values"})) - )) + sampler = MockSampler( + MockSamplerOptions( + settings=None, + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + kvs={"custom-key": "value", "sw-keys": "sw-values"} + ) + ), + ) + ) generator = RandomIdGenerator() - sample = sampler.should_sample(None, generator.generate_trace_id(), "respects_x_trace_options_keys_and_values") - assert sample.attributes == {"custom-key": "value", SW_KEYS_ATTRIBUTE: "sw-values"} - assert "trigger-trace=not-requested" in sampler.response_headers.x_trace_options_response + sample = sampler.should_sample( + None, + generator.generate_trace_id(), + "respects_x_trace_options_keys_and_values", + ) + assert sample.attributes == { + "custom-key": "value", + SW_KEYS_ATTRIBUTE: "sw-values", + } + assert ( + "trigger-trace=not-requested" + in sampler.response_headers.x_trace_options_response + ) def test_ignores_trigger_trace(self): - sampler = MockSampler(MockSamplerOptions( - settings=None, - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, kvs={"custom-key": "value", "invalid-keys": "value"})) - )) + sampler = MockSampler( + MockSamplerOptions( + settings=None, + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + kvs={"custom-key": "value", "invalid-keys": "value"}, + ) + ), + ) + ) generator = RandomIdGenerator() - sample = sampler.should_sample(None, generator.generate_trace_id(), "ignores_trigger_trace") + sample = sampler.should_sample( + None, generator.generate_trace_id(), "ignores_trigger_trace" + ) assert sample.attributes == {"custom-key": "value"} - assert "trigger-trace=settings-not-available" in sampler.response_headers.x_trace_options_response - assert "ignored=invalid-key" in sampler.response_headers.x_trace_options_response + assert ( + "trigger-trace=settings-not-available" + in sampler.response_headers.x_trace_options_response + ) + assert ( + "ignored=invalid-key" + in sampler.response_headers.x_trace_options_response + ) + class TestEntrySpan: def test_sw_w3c_tracestate_with_x_trace_options_response(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True, other_trace_state=True, xtrace_options_response=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_parent_sampled", None, None, None, None) - assert "vendor2=value2,vendor1=value1" in sample.attributes.get(TRACESTATE_CAPTURE_ATTRIBUTE) + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, + is_remote=True, + sw=True, + other_trace_state=True, + xtrace_options_response=True, + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_parent_sampled", + None, + None, + None, + None, + ) + assert "vendor2=value2,vendor1=value1" in sample.attributes.get( + TRACESTATE_CAPTURE_ATTRIBUTE + ) assert "sw=" in sample.attributes.get(TRACESTATE_CAPTURE_ATTRIBUTE) - assert INTL_SWO_X_OPTIONS_RESPONSE_KEY not in sample.attributes.get(TRACESTATE_CAPTURE_ATTRIBUTE) + assert INTL_SWO_X_OPTIONS_RESPONSE_KEY not in sample.attributes.get( + TRACESTATE_CAPTURE_ATTRIBUTE + ) def test_sw_w3c_tracestate_without_x_trace_options_response(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True, other_trace_state=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_parent_sampled", None, None, None, None) - assert "vendor2=value2,vendor1=value1" in sample.attributes.get(TRACESTATE_CAPTURE_ATTRIBUTE) + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, + is_remote=True, + sw=True, + other_trace_state=True, + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_parent_sampled", + None, + None, + None, + None, + ) + assert "vendor2=value2,vendor1=value1" in sample.attributes.get( + TRACESTATE_CAPTURE_ATTRIBUTE + ) assert "sw=" in sample.attributes.get(TRACESTATE_CAPTURE_ATTRIBUTE) - assert INTL_SWO_X_OPTIONS_RESPONSE_KEY not in sample.attributes.get(TRACESTATE_CAPTURE_ATTRIBUTE) + assert INTL_SWO_X_OPTIONS_RESPONSE_KEY not in sample.attributes.get( + TRACESTATE_CAPTURE_ATTRIBUTE + ) class TestEntrySpanWithValidSwContextXTraceOptions: def test_respects_keys_and_values(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(kvs={"custom-key": "value", "sw-keys": "sw-values"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_keys_and_values") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + kvs={"custom-key": "value", "sw-keys": "sw-values"} + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_keys_and_values", + ) assert sample.attributes.get("custom-key") == "value" assert sample.attributes.get(SW_KEYS_ATTRIBUTE) == "sw-values" - assert "trigger-trace=not-requested" in sampler.response_headers.x_trace_options_response + assert ( + "trigger-trace=not-requested" + in sampler.response_headers.x_trace_options_response + ) def test_ignores_trigger_trace(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, kvs={"custom-key": "value", "invalid-keys": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "ignores_trigger_trace") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + kvs={"custom-key": "value", "invalid-keys": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "ignores_trigger_trace", + ) assert sample.attributes.get("custom-key") == "value" - assert "trigger-trace=ignored" in sampler.response_headers.x_trace_options_response - assert "ignored=invalid-key" in sampler.response_headers.x_trace_options_response + assert ( + "trigger-trace=ignored" + in sampler.response_headers.x_trace_options_response + ) + assert ( + "ignored=invalid-key" + in sampler.response_headers.x_trace_options_response + ) class TestEntrySpanWithValidSwContextSampleThroughAlwaysSet: @pytest.fixture() def sample_through_always_set(self): - return MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) + return MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) def test_respects_parent_sampled(self, sample_through_always_set): - ctxt = sample_through_always_set._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True) - sample = sample_through_always_set.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_parent_sampled") + ctxt = sample_through_always_set._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True + ) + sample = sample_through_always_set.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_parent_sampled", + ) assert sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("sw.tracestate_parent_id") == format( - get_current_span(ctxt).get_span_context().span_id, "016x") - check_counters(sample_through_always_set, - ["trace.service.request_count", "trace.service.tracecount", "trace.service.through_trace_count"]) + get_current_span(ctxt).get_span_context().span_id, "016x" + ) + check_counters( + sample_through_always_set, + [ + "trace.service.request_count", + "trace.service.tracecount", + "trace.service.through_trace_count", + ], + ) def test_respects_parent_not_sampled(self, sample_through_always_set): - ctxt = sample_through_always_set._create_parent(trace_flags=TraceFlags.DEFAULT, is_remote=True, sw=True) - sample = sample_through_always_set.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_parent_not_sampled") + ctxt = sample_through_always_set._create_parent( + trace_flags=TraceFlags.DEFAULT, is_remote=True, sw=True + ) + sample = sample_through_always_set.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_parent_not_sampled", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("sw.tracestate_parent_id") == format( - get_current_span(ctxt).get_span_context().span_id, "016x") - check_counters(sample_through_always_set, ["trace.service.request_count"]) + get_current_span(ctxt).get_span_context().span_id, "016x" + ) + check_counters( + sample_through_always_set, ["trace.service.request_count"] + ) - def test_respects_sw_sampled_over_w3c_not_sampled(self, sample_through_always_set): - ctxt = sample_through_always_set._create_parent(trace_flags=TraceFlags.DEFAULT, is_remote=True, sw="inverse") - sample = sample_through_always_set.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_sw_sampled_over_w3c_not_sampled") + def test_respects_sw_sampled_over_w3c_not_sampled( + self, sample_through_always_set + ): + ctxt = sample_through_always_set._create_parent( + trace_flags=TraceFlags.DEFAULT, is_remote=True, sw="inverse" + ) + sample = sample_through_always_set.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_sw_sampled_over_w3c_not_sampled", + ) assert sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("sw.tracestate_parent_id") == format( - get_current_span(ctxt).get_span_context().span_id, "016x") - check_counters(sample_through_always_set, - ["trace.service.request_count", "trace.service.tracecount", "trace.service.through_trace_count"]) - - def test_respects_sw_not_sampled_over_w3c_sampled(self, sample_through_always_set): - ctxt = sample_through_always_set._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw="inverse") - sample = sample_through_always_set.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_sw_not_sampled_over_w3c_sampled") + get_current_span(ctxt).get_span_context().span_id, "016x" + ) + check_counters( + sample_through_always_set, + [ + "trace.service.request_count", + "trace.service.tracecount", + "trace.service.through_trace_count", + ], + ) + + def test_respects_sw_not_sampled_over_w3c_sampled( + self, sample_through_always_set + ): + ctxt = sample_through_always_set._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True, sw="inverse" + ) + sample = sample_through_always_set.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_sw_not_sampled_over_w3c_sampled", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("sw.tracestate_parent_id") == format( - get_current_span(ctxt).get_span_context().span_id, "016x") - check_counters(sample_through_always_set, ["trace.service.request_count"]) + get_current_span(ctxt).get_span_context().span_id, "016x" + ) + check_counters( + sample_through_always_set, ["trace.service.request_count"] + ) def test_respects_sw_random_trace_id_not_sampled_02( self, sample_through_always_set @@ -518,7 +838,9 @@ def test_respects_sw_random_trace_id_not_sampled_02( get_current_span(ctxt).get_span_context().span_id, "016x" ) assert "-02" in sample.attributes.get(TRACESTATE_CAPTURE_ATTRIBUTE) - check_counters(sample_through_always_set, ["trace.service.request_count"]) + check_counters( + sample_through_always_set, ["trace.service.request_count"] + ) def test_respects_sw_random_trace_id_sampled_03( self, sample_through_always_set @@ -557,44 +879,62 @@ def test_respects_sw_random_trace_id_sampled_03( class TestEntrySpanWithValidSwContextSampleThroughAlwaysUnset: - def test_records_but_does_not_sample_when_SAMPLE_START_set(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_sw_not_sampled_over_w3c_sampled") + def test_records_but_does_not_sample_when_sample_start_set(self): + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_sw_not_sampled_over_w3c_sampled", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() check_counters(sampler, ["trace.service.request_count"]) - def test_does_not_record_or_sample_when_SAMPLE_START_unset(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.OK, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "does_not_record_or_sample_when_SAMPLE_START_unset") + def test_does_not_record_or_sample_when_sample_start_unset(self): + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.OK, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True, sw=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "does_not_record_or_sample_when_SAMPLE_START_unset", + ) assert not sample.decision.is_sampled() assert not sample.decision.is_recording() check_counters(sampler, ["trace.service.request_count"]) @@ -602,90 +942,157 @@ def test_does_not_record_or_sample_when_SAMPLE_START_unset(self): class TestTriggerTraceRequestedTriggeredTraceSetUnsigned: def test_records_and_samples_when_there_is_capacity(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, - buckets={ - BucketType.TRIGGER_STRICT: BucketSettings(capacity=10, rate=5), - BucketType.TRIGGER_RELAXED: BucketSettings(capacity=0, rate=0) - }, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, kvs={"custom-key": "value", "sw-keys": "sw-values"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "records_and_samples_when_there_is_capacity") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, + buckets={ + BucketType.TRIGGER_STRICT: BucketSettings( + capacity=10, rate=5 + ), + BucketType.TRIGGER_RELAXED: BucketSettings( + capacity=0, rate=0 + ), + }, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + kvs={"custom-key": "value", "sw-keys": "sw-values"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "records_and_samples_when_there_is_capacity", + ) assert sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("custom-key") == "value" assert sample.attributes.get(SW_KEYS_ATTRIBUTE) == "sw-values" assert sample.attributes.get(BUCKET_CAPACITY_ATTRIBUTE) == 10 assert sample.attributes.get(BUCKET_RATE_ATTRIBUTE) == 5 - assert "trigger-trace=ok" in sampler.response_headers.x_trace_options_response - check_counters(sampler, ["trace.service.request_count", "trace.service.tracecount", - "trace.service.triggered_trace_count"]) + assert ( + "trigger-trace=ok" + in sampler.response_headers.x_trace_options_response + ) + check_counters( + sampler, + [ + "trace.service.request_count", + "trace.service.tracecount", + "trace.service.triggered_trace_count", + ], + ) def test_records_but_does_not_sample_when_there_is_no_capacity(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, - buckets={ - BucketType.TRIGGER_STRICT: BucketSettings(capacity=0, rate=0), - BucketType.TRIGGER_RELAXED: BucketSettings(capacity=20, rate=10) - }, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, kvs={"custom-key": "value", "invalid-keys": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "records_but_does_not_sample_when_there_is_no_capacity") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, + buckets={ + BucketType.TRIGGER_STRICT: BucketSettings( + capacity=0, rate=0 + ), + BucketType.TRIGGER_RELAXED: BucketSettings( + capacity=20, rate=10 + ), + }, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + kvs={"custom-key": "value", "invalid-keys": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "records_but_does_not_sample_when_there_is_no_capacity", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("custom-key") == "value" assert sample.attributes.get(BUCKET_CAPACITY_ATTRIBUTE) == 0 assert sample.attributes.get(BUCKET_RATE_ATTRIBUTE) == 0 - assert "trigger-trace=rate-exceeded" in sampler.response_headers.x_trace_options_response - assert "ignored=invalid-key" in sampler.response_headers.x_trace_options_response + assert ( + "trigger-trace=rate-exceeded" + in sampler.response_headers.x_trace_options_response + ) + assert ( + "ignored=invalid-key" + in sampler.response_headers.x_trace_options_response + ) check_counters(sampler, ["trace.service.request_count"]) class TestTriggerTraceRequestedTriggeredTraceSetSigned: def test_records_and_samples_when_there_is_capacity(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, - buckets={ - BucketType.TRIGGER_STRICT: BucketSettings(capacity=0, rate=0), - BucketType.TRIGGER_RELAXED: BucketSettings(capacity=20, rate=10) - }, - signature_key="key", - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, signature=True, signature_key="key", - kvs={"custom-key": "value", "sw-keys": "sw-values"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "records_and_samples_when_there_is_capacity") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, + buckets={ + BucketType.TRIGGER_STRICT: BucketSettings( + capacity=0, rate=0 + ), + BucketType.TRIGGER_RELAXED: BucketSettings( + capacity=20, rate=10 + ), + }, + signature_key="key", + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + signature=True, + signature_key="key", + kvs={"custom-key": "value", "sw-keys": "sw-values"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "records_and_samples_when_there_is_capacity", + ) assert sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("custom-key") == "value" @@ -693,237 +1100,383 @@ def test_records_and_samples_when_there_is_capacity(self): assert sample.attributes.get(BUCKET_CAPACITY_ATTRIBUTE) == 20 assert sample.attributes.get(BUCKET_RATE_ATTRIBUTE) == 10 assert "auth=ok" in sampler.response_headers.x_trace_options_response - assert "trigger-trace=ok" in sampler.response_headers.x_trace_options_response - check_counters(sampler, ["trace.service.request_count", "trace.service.tracecount", - "trace.service.triggered_trace_count"]) + assert ( + "trigger-trace=ok" + in sampler.response_headers.x_trace_options_response + ) + check_counters( + sampler, + [ + "trace.service.request_count", + "trace.service.tracecount", + "trace.service.triggered_trace_count", + ], + ) def test_records_but_does_not_sample_when_there_is_no_capacity(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, - buckets={ - BucketType.TRIGGER_STRICT: BucketSettings(capacity=10, rate=5), - BucketType.TRIGGER_RELAXED: BucketSettings(capacity=0, rate=0) - }, - signature_key="key", - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, signature=True, signature_key="key", - kvs={"custom-key": "value", "invalid-keys": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "records_but_does_not_sample_when_there_is_no_capacity") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START | Flags.TRIGGERED_TRACE, + buckets={ + BucketType.TRIGGER_STRICT: BucketSettings( + capacity=10, rate=5 + ), + BucketType.TRIGGER_RELAXED: BucketSettings( + capacity=0, rate=0 + ), + }, + signature_key="key", + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + signature=True, + signature_key="key", + kvs={"custom-key": "value", "invalid-keys": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "records_but_does_not_sample_when_there_is_no_capacity", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("custom-key") == "value" assert sample.attributes.get(BUCKET_CAPACITY_ATTRIBUTE) == 0 assert sample.attributes.get(BUCKET_RATE_ATTRIBUTE) == 0 - assert "trigger-trace=rate-exceeded" in sampler.response_headers.x_trace_options_response - assert "ignored=invalid-key" in sampler.response_headers.x_trace_options_response + assert ( + "trigger-trace=rate-exceeded" + in sampler.response_headers.x_trace_options_response + ) + assert ( + "ignored=invalid-key" + in sampler.response_headers.x_trace_options_response + ) check_counters(sampler, ["trace.service.request_count"]) class TestTriggerTraceRequestedTriggeredTraceUnset: - def test_record_but_does_not_sample_when_TRIGGERED_TRACE_unset(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, kvs={"custom-key": "value", "invalid-keys": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "record_but_does_not_sample_when_TRIGGERED_TRACE_unset") + def test_record_but_does_not_sample_when_triggered_trace_unset(self): + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + kvs={"custom-key": "value", "invalid-keys": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "record_but_does_not_sample_when_TRIGGERED_TRACE_unset", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get("custom-key") == "value" - assert "trigger-trace=trigger-tracing-disabled" in sampler.response_headers.x_trace_options_response - assert "ignored=invalid-key" in sampler.response_headers.x_trace_options_response + assert ( + "trigger-trace=trigger-tracing-disabled" + in sampler.response_headers.x_trace_options_response + ) + assert ( + "ignored=invalid-key" + in sampler.response_headers.x_trace_options_response + ) check_counters(sampler, ["trace.service.request_count"]) class TestTriggerTraceRequestedDiceRoll: def test_respects_x_trace_options_keys_and_values(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(kvs={"custom-key": "value", "sw-keys": "sw-values"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.DEFAULT, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "respects_x_trace_options_keys_and_values") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + kvs={"custom-key": "value", "sw-keys": "sw-values"} + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.DEFAULT, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "respects_x_trace_options_keys_and_values", + ) assert sample.attributes.get("custom-key") == "value" assert sample.attributes.get(SW_KEYS_ATTRIBUTE) == "sw-values" - assert "trigger-trace=not-requested" in sampler.response_headers.x_trace_options_response - - def test_records_and_samples_when_dice_success_and_sufficient_capacity(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=1_000_000, - sample_source=SampleSource.REMOTE, - flags=Flags.SAMPLE_START, - buckets={ - BucketType.DEFAULT: BucketSettings(capacity=10, rate=5), - }, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) + assert ( + "trigger-trace=not-requested" + in sampler.response_headers.x_trace_options_response + ) + + def test_records_and_samples_when_dice_success_and_sufficient_capacity( + self, + ): + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=1_000_000, + sample_source=SampleSource.REMOTE, + flags=Flags.SAMPLE_START, + buckets={ + BucketType.DEFAULT: BucketSettings( + capacity=10, rate=5 + ), + }, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) generator = RandomIdGenerator() - sample = sampler.should_sample(None, generator.generate_trace_id(), - "records_and_samples_when_dice_success_and_sufficient_capacity") + sample = sampler.should_sample( + None, + generator.generate_trace_id(), + "records_and_samples_when_dice_success_and_sufficient_capacity", + ) assert sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get(SAMPLE_RATE_ATTRIBUTE) == 1_000_000 assert sample.attributes.get(SAMPLE_SOURCE_ATTRIBUTE) == 6 assert sample.attributes.get(BUCKET_CAPACITY_ATTRIBUTE) == 10 assert sample.attributes.get(BUCKET_RATE_ATTRIBUTE) == 5 - check_counters(sampler, - ["trace.service.request_count", "trace.service.samplecount", "trace.service.tracecount"]) - - def test_records_but_does_not_sample_when_dice_success_but_insufficient_capacity(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=1_000_000, - sample_source=SampleSource.REMOTE, - flags=Flags.SAMPLE_START, - buckets={ - BucketType.DEFAULT: BucketSettings(capacity=0, rate=0), - }, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) + check_counters( + sampler, + [ + "trace.service.request_count", + "trace.service.samplecount", + "trace.service.tracecount", + ], + ) + + def test_records_but_does_not_sample_when_dice_success_but_insufficient_capacity( + self, + ): + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=1_000_000, + sample_source=SampleSource.REMOTE, + flags=Flags.SAMPLE_START, + buckets={ + BucketType.DEFAULT: BucketSettings(capacity=0, rate=0), + }, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) generator = RandomIdGenerator() - sample = sampler.should_sample(None, generator.generate_trace_id(), - "records_but_does_not_sample_when_dice_success_but_insufficient_capacity") + sample = sampler.should_sample( + None, + generator.generate_trace_id(), + "records_but_does_not_sample_when_dice_success_but_insufficient_capacity", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get(SAMPLE_RATE_ATTRIBUTE) == 1_000_000 assert sample.attributes.get(SAMPLE_SOURCE_ATTRIBUTE) == 6 assert sample.attributes.get(BUCKET_CAPACITY_ATTRIBUTE) == 0 assert sample.attributes.get(BUCKET_RATE_ATTRIBUTE) == 0 - check_counters(sampler, ["trace.service.request_count", "trace.service.samplecount", - "trace.service.tokenbucket_exhaustion_count"]) + check_counters( + sampler, + [ + "trace.service.request_count", + "trace.service.samplecount", + "trace.service.tokenbucket_exhaustion_count", + ], + ) def test_records_but_does_not_sample_when_dice_failure(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START, - buckets={ - BucketType.DEFAULT: BucketSettings(capacity=10, rate=5) - }, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_START, + buckets={ + BucketType.DEFAULT: BucketSettings(capacity=10, rate=5) + }, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) generator = RandomIdGenerator() - sample = sampler.should_sample(None, generator.generate_trace_id(), - "records_but_does_not_sample_when_dice_failure") + sample = sampler.should_sample( + None, + generator.generate_trace_id(), + "records_but_does_not_sample_when_dice_failure", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() assert sample.attributes.get(SAMPLE_RATE_ATTRIBUTE) == 0 assert sample.attributes.get(SAMPLE_SOURCE_ATTRIBUTE) == 2 assert BUCKET_CAPACITY_ATTRIBUTE not in sample.attributes assert BUCKET_RATE_ATTRIBUTE not in sample.attributes - check_counters(sampler, ["trace.service.request_count", "trace.service.samplecount"]) + check_counters( + sampler, + ["trace.service.request_count", "trace.service.samplecount"], + ) class TestTriggerTraceRequestedSampleStartUnset: def test_ignores_trigger_trace(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.OK, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers( - MakeRequestHeaders(trigger_trace=True, kvs={"custom-key": "value", "invalid-keys": "value"})) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "ignores_trigger_trace") + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.OK, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers( + MakeRequestHeaders( + trigger_trace=True, + kvs={"custom-key": "value", "invalid-keys": "value"}, + ) + ), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "ignores_trigger_trace", + ) assert sample.attributes.get("custom-key") == "value" - assert "trigger-trace=tracing-disabled" in sampler.response_headers.x_trace_options_response - assert "ignored=invalid-key" in sampler.response_headers.x_trace_options_response - - def test_records_when_SAMPLE_THROUGH_ALWAYS_set(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_THROUGH_ALWAYS, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=True, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "records_when_SAMPLE_THROUGH_ALWAYS_set") + assert ( + "trigger-trace=tracing-disabled" + in sampler.response_headers.x_trace_options_response + ) + assert ( + "ignored=invalid-key" + in sampler.response_headers.x_trace_options_response + ) + + def test_records_when_sample_through_always_set(self): + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.SAMPLE_THROUGH_ALWAYS, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=True, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "records_when_SAMPLE_THROUGH_ALWAYS_set", + ) assert not sample.decision.is_sampled() assert sample.decision.is_recording() check_counters(sampler, ["trace.service.request_count"]) - def test_does_not_record_when_SAMPLE_THROUGH_ALWAYS_unset(self): - sampler = MockSampler(MockSamplerOptions( - settings=Settings( - sample_rate=0, - sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.OK, - buckets={}, - signature_key=None, - timestamp=int(time.time()), - ttl=10 - ), - local_settings=LocalSettings(trigger_mode=False, tracing_mode=None), - request_headers=make_request_headers(MakeRequestHeaders()) - )) - ctxt = sampler._create_parent(trace_flags=TraceFlags.SAMPLED, is_remote=True) - sample = sampler.should_sample(ctxt, get_current_span(ctxt).get_span_context().trace_id, - "does_not_record_when_SAMPLE_THROUGH_ALWAYS_unset") + def test_does_not_record_when_sample_through_always_unset(self): + sampler = MockSampler( + MockSamplerOptions( + settings=Settings( + sample_rate=0, + sample_source=SampleSource.LOCAL_DEFAULT, + flags=Flags.OK, + buckets={}, + signature_key=None, + timestamp=int(time.time()), + ttl=10, + ), + local_settings=LocalSettings( + trigger_mode=False, tracing_mode=None + ), + request_headers=make_request_headers(MakeRequestHeaders()), + ) + ) + ctxt = sampler._create_parent( + trace_flags=TraceFlags.SAMPLED, is_remote=True + ) + sample = sampler.should_sample( + ctxt, + get_current_span(ctxt).get_span_context().trace_id, + "does_not_record_when_SAMPLE_THROUGH_ALWAYS_unset", + ) assert not sample.decision.is_sampled() assert not sample.decision.is_recording() check_counters(sampler, ["trace.service.request_count"]) diff --git a/tests/unit/test_oboe/test_sampler.py b/tests/unit/test_oboe/test_sampler.py index ee71fdc23..03b87aa9c 100644 --- a/tests/unit/test_oboe/test_sampler.py +++ b/tests/unit/test_oboe/test_sampler.py @@ -5,39 +5,72 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. from __future__ import annotations - import time from typing import Any from opentelemetry import trace -from opentelemetry.sdk.metrics import MeterProvider, AlwaysOnExemplarFilter +from opentelemetry.sdk.metrics import AlwaysOnExemplarFilter, MeterProvider from opentelemetry.sdk.metrics._internal.export import InMemoryMetricReader from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter -from opentelemetry.semconv._incubating.attributes.http_attributes import HTTP_METHOD, HTTP_STATUS_CODE, HTTP_SCHEME, \ - HTTP_TARGET -from opentelemetry.semconv._incubating.attributes.net_attributes import NET_HOST_NAME -from opentelemetry.semconv.attributes.http_attributes import HTTP_REQUEST_METHOD, HTTP_RESPONSE_STATUS_CODE +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.semconv._incubating.attributes.http_attributes import ( + HTTP_METHOD, + HTTP_SCHEME, + HTTP_STATUS_CODE, + HTTP_TARGET, +) +from opentelemetry.semconv._incubating.attributes.net_attributes import ( + NET_HOST_NAME, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_REQUEST_METHOD, + HTTP_RESPONSE_STATUS_CODE, +) from opentelemetry.semconv.attributes.server_attributes import SERVER_ADDRESS -from opentelemetry.semconv.attributes.url_attributes import URL_SCHEME, URL_PATH +from opentelemetry.semconv.attributes.url_attributes import ( + URL_PATH, + URL_SCHEME, +) from opentelemetry.trace import SpanKind from solarwinds_apm.oboe.configuration import Configuration, TransactionSetting -from solarwinds_apm.oboe.sampler import http_span_metadata, parse_settings, Sampler -from solarwinds_apm.oboe.settings import Settings, SampleSource, Flags, BucketType, BucketSettings +from solarwinds_apm.oboe.sampler import ( + Sampler, + http_span_metadata, + parse_settings, +) +from solarwinds_apm.oboe.settings import ( + BucketSettings, + BucketType, + Flags, + SampleSource, + Settings, +) class MockSampler(Sampler): - def __init__(self, meter_provider: MeterProvider, config: Configuration, initial: Any): - super().__init__(meter_provider=meter_provider, config=config, initial=initial) + def __init__( + self, + meter_provider: MeterProvider, + config: Configuration, + initial: Any, + ): + super().__init__( + meter_provider=meter_provider, config=config, initial=initial + ) def __str__(self): raise Exception("Test sampler") -def options(tracing: bool | None, trigger_trace: bool, - transaction_settings: list[TransactionSetting]) -> Configuration: +def options( + tracing: bool | None, + trigger_trace: bool, + transaction_settings: list[TransactionSetting], +) -> Configuration: return Configuration( tracing_mode=tracing, trigger_trace_enabled=trigger_trace, @@ -53,7 +86,9 @@ def options(tracing: bool | None, trigger_trace: bool, def settings(enabled: bool, signature_key: str | None): return { "value": 1_000_000, - "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE" if enabled else "", + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE" + if enabled + else "", "arguments": { "BucketCapacity": 10, "BucketRate": 1, @@ -64,7 +99,7 @@ def settings(enabled: bool, signature_key: str | None): "SignatureKey": signature_key, }, "timestamp": int(time.time()), - "ttl": 60 + "ttl": 60, } @@ -137,7 +172,7 @@ def test_handles_legacy_http_server_spans_properly(self): class TestParseSettingsName: - def test_correctly_parses_JSON_settings(self): + def test_correctly_parses_json_settings(self): timestamp = int(time.time()) settings = { "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,TRIGGER_TRACE,OVERRIDE", @@ -153,41 +188,54 @@ def test_correctly_parses_JSON_settings(self): }, "timestamp": timestamp, "ttl": 120, - "warning": "warning" + "warning": "warning", } output, warnings = parse_settings(settings) assert output == Settings( sample_rate=500_000, sample_source=SampleSource.REMOTE, - flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS | Flags.TRIGGERED_TRACE | Flags.OVERRIDE, + flags=Flags.SAMPLE_START + | Flags.SAMPLE_THROUGH_ALWAYS + | Flags.TRIGGERED_TRACE + | Flags.OVERRIDE, buckets={ BucketType.DEFAULT: BucketSettings(capacity=0.2, rate=0.1), - BucketType.TRIGGER_RELAXED: BucketSettings(capacity=20, rate=10), + BucketType.TRIGGER_RELAXED: BucketSettings( + capacity=20, rate=10 + ), BucketType.TRIGGER_STRICT: BucketSettings(capacity=2, rate=1), }, signature_key="key", timestamp=timestamp, - ttl=120 + ttl=120, ) assert warnings == "warning" class TestSamplerName: - def test_respects_enabled_settings_when_no_config_or_transaction_settings(self): + def test_respects_enabled_settings_when_no_config_or_transaction_settings( + self, + ): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=None, trigger_trace=False, transaction_settings=[]), - initial=settings(enabled=True, signature_key=None) + config=options( + tracing=None, trigger_trace=False, transaction_settings=[] + ), + initial=settings(enabled=True, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("respects_enabled_settings_when_no_config_or_transaction_settings", - tracer_provider=tracer_provider) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "respects_enabled_settings_when_no_config_or_transaction_settings", + tracer_provider=tracer_provider, + ) with tracer.start_as_current_span("test") as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() @@ -199,21 +247,29 @@ def test_respects_enabled_settings_when_no_config_or_transaction_settings(self): "BucketRate": 1, } - def test_respects_disabled_settings_when_no_config_or_transaction_settings(self): + def test_respects_disabled_settings_when_no_config_or_transaction_settings( + self, + ): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=None, trigger_trace=True, transaction_settings=[]), - initial=settings(enabled=False, signature_key=None) + config=options( + tracing=None, trigger_trace=True, transaction_settings=[] + ), + initial=settings(enabled=False, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("respects_disabled_settings_when_no_config_or_transaction_settings", - tracer_provider=tracer_provider) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "respects_disabled_settings_when_no_config_or_transaction_settings", + tracer_provider=tracer_provider, + ) with tracer.start_as_current_span("test") as span: assert not span.is_recording() spans = memory_exporter.get_finished_spans() @@ -222,18 +278,24 @@ def test_respects_disabled_settings_when_no_config_or_transaction_settings(self) def test_respects_enabled_config_when_no_transaction_settings(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=True, trigger_trace=True, transaction_settings=[]), - initial=settings(enabled=False, signature_key=None) + config=options( + tracing=True, trigger_trace=True, transaction_settings=[] + ), + initial=settings(enabled=False, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("respects_enabled_config_when_no_transaction_settings", - tracer_provider=tracer_provider) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "respects_enabled_config_when_no_transaction_settings", + tracer_provider=tracer_provider, + ) with tracer.start_as_current_span("test") as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() @@ -248,18 +310,24 @@ def test_respects_enabled_config_when_no_transaction_settings(self): def test_respects_disabled_config_when_no_transaction_settings(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=False, trigger_trace=False, transaction_settings=[]), - initial=settings(enabled=True, signature_key=None) + config=options( + tracing=False, trigger_trace=False, transaction_settings=[] + ), + initial=settings(enabled=True, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("respects_disabled_config_when_no_transaction_settings", - tracer_provider=tracer_provider) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "respects_disabled_config_when_no_transaction_settings", + tracer_provider=tracer_provider, + ) with tracer.start_as_current_span("test") as span: assert not span.is_recording() spans = memory_exporter.get_finished_spans() @@ -268,18 +336,28 @@ def test_respects_disabled_config_when_no_transaction_settings(self): def test_respects_enabled_matching_transaction_setting(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=False, trigger_trace=False, - transaction_settings=[TransactionSetting(tracing=True, matcher=lambda s: True)]), - initial=settings(enabled=False, signature_key=None) + config=options( + tracing=False, + trigger_trace=False, + transaction_settings=[ + TransactionSetting(tracing=True, matcher=lambda s: True) + ], + ), + initial=settings(enabled=False, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("respects_enabled_matching_transaction_setting", tracer_provider=tracer_provider) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "respects_enabled_matching_transaction_setting", + tracer_provider=tracer_provider, + ) with tracer.start_as_current_span("test") as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() @@ -294,18 +372,28 @@ def test_respects_enabled_matching_transaction_setting(self): def test_respects_disabled_matching_transaction_setting(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=True, trigger_trace=True, - transaction_settings=[TransactionSetting(tracing=False, matcher=lambda s: True)]), - initial=settings(enabled=True, signature_key=None) + config=options( + tracing=True, + trigger_trace=True, + transaction_settings=[ + TransactionSetting(tracing=False, matcher=lambda s: True) + ], + ), + initial=settings(enabled=True, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("respects_disabled_matching_transaction_setting", tracer_provider=tracer_provider) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "respects_disabled_matching_transaction_setting", + tracer_provider=tracer_provider, + ) with tracer.start_as_current_span("test") as span: assert not span.is_recording() spans = memory_exporter.get_finished_spans() @@ -314,20 +402,29 @@ def test_respects_disabled_matching_transaction_setting(self): def test_respects_first_matching_transaction_setting(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=False, trigger_trace=False, transaction_settings=[ - TransactionSetting(tracing=True, matcher=lambda s: True), - TransactionSetting(tracing=False, matcher=lambda s: True), - ]), - initial=settings(enabled=False, signature_key=None) + config=options( + tracing=False, + trigger_trace=False, + transaction_settings=[ + TransactionSetting(tracing=True, matcher=lambda s: True), + TransactionSetting(tracing=False, matcher=lambda s: True), + ], + ), + initial=settings(enabled=False, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("respects_first_matching_transaction_setting", tracer_provider=tracer_provider) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "respects_first_matching_transaction_setting", + tracer_provider=tracer_provider, + ) with tracer.start_as_current_span("test") as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() @@ -342,20 +439,32 @@ def test_respects_first_matching_transaction_setting(self): def test_matches_non_http_spans(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=False, trigger_trace=False, transaction_settings=[ - TransactionSetting(tracing=True, matcher=lambda s: s == "CLIENT:test"), - ]), - initial=settings(enabled=False, signature_key=None) + config=options( + tracing=False, + trigger_trace=False, + transaction_settings=[ + TransactionSetting( + tracing=True, matcher=lambda s: s == "CLIENT:test" + ), + ], + ), + initial=settings(enabled=False, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("matches_non_http_spans", tracer_provider=tracer_provider) - with tracer.start_as_current_span("test", kind=SpanKind.CLIENT) as span: + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "matches_non_http_spans", tracer_provider=tracer_provider + ) + with tracer.start_as_current_span( + "test", kind=SpanKind.CLIENT + ) as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() assert len(spans) == 1 @@ -369,59 +478,89 @@ def test_matches_non_http_spans(self): def test_matches_http_spans(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=False, trigger_trace=False, transaction_settings=[ - TransactionSetting(tracing=True, matcher=lambda s: s == "http://localhost/test"), - ]), - initial=settings(enabled=False, signature_key=None) + config=options( + tracing=False, + trigger_trace=False, + transaction_settings=[ + TransactionSetting( + tracing=True, + matcher=lambda s: s == "http://localhost/test", + ), + ], + ), + initial=settings(enabled=False, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("matches_http_spans", tracer_provider=tracer_provider) - with tracer.start_as_current_span("test", kind=SpanKind.SERVER, attributes={ - HTTP_METHOD: "GET", - URL_SCHEME: "http", - SERVER_ADDRESS: "localhost", - URL_PATH: "/test", - }) as span: + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "matches_http_spans", tracer_provider=tracer_provider + ) + with tracer.start_as_current_span( + "test", + kind=SpanKind.SERVER, + attributes={ + HTTP_METHOD: "GET", + URL_SCHEME: "http", + SERVER_ADDRESS: "localhost", + URL_PATH: "/test", + }, + ) as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() assert len(spans) == 1 - assert spans[0].attributes['SampleRate'] == 1_000_000 - assert spans[0].attributes['SampleSource'] == 6 - assert spans[0].attributes['BucketCapacity'] == 10 - assert spans[0].attributes['BucketRate'] == 1 + assert spans[0].attributes["SampleRate"] == 1_000_000 + assert spans[0].attributes["SampleSource"] == 6 + assert spans[0].attributes["BucketCapacity"] == 10 + assert spans[0].attributes["BucketRate"] == 1 def test_matches_deprecated_http_spans(self): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], - exemplar_filter=AlwaysOnExemplarFilter() + exemplar_filter=AlwaysOnExemplarFilter(), ) sampler = MockSampler( meter_provider=meter_provider, - config=options(tracing=False, trigger_trace=False, transaction_settings=[ - TransactionSetting(tracing=True, matcher=lambda s: s == "http://localhost/test"), - ]), - initial=settings(enabled=False, signature_key=None) + config=options( + tracing=False, + trigger_trace=False, + transaction_settings=[ + TransactionSetting( + tracing=True, + matcher=lambda s: s == "http://localhost/test", + ), + ], + ), + initial=settings(enabled=False, signature_key=None), ) memory_exporter = InMemorySpanExporter() tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor(span_processor=SimpleSpanProcessor(span_exporter=memory_exporter)) - tracer = trace.get_tracer("matches_deprecated_http_spans", tracer_provider=tracer_provider) - with tracer.start_as_current_span("test", kind=SpanKind.SERVER, attributes={ - HTTP_REQUEST_METHOD: "GET", - URL_SCHEME: "http", - SERVER_ADDRESS: "localhost", - URL_PATH: "/test", - }) as span: + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer( + "matches_deprecated_http_spans", tracer_provider=tracer_provider + ) + with tracer.start_as_current_span( + "test", + kind=SpanKind.SERVER, + attributes={ + HTTP_REQUEST_METHOD: "GET", + URL_SCHEME: "http", + SERVER_ADDRESS: "localhost", + URL_PATH: "/test", + }, + ) as span: assert span.is_recording() spans = memory_exporter.get_finished_spans() assert len(spans) == 1 - assert spans[0].attributes['SampleRate'] == 1_000_000 - assert spans[0].attributes['SampleSource'] == 6 - assert spans[0].attributes['BucketCapacity'] == 10 - assert spans[0].attributes['BucketRate'] == 1 + assert spans[0].attributes["SampleRate"] == 1_000_000 + assert spans[0].attributes["SampleSource"] == 6 + assert spans[0].attributes["BucketCapacity"] == 10 + assert spans[0].attributes["BucketRate"] == 1 diff --git a/tests/unit/test_oboe/test_settings.py b/tests/unit/test_oboe/test_settings.py index eca6f8c03..452522c51 100644 --- a/tests/unit/test_oboe/test_settings.py +++ b/tests/unit/test_oboe/test_settings.py @@ -6,23 +6,29 @@ import time -from solarwinds_apm.oboe.settings import Flags, LocalSettings, merge, SampleSource, Settings, TracingMode +from solarwinds_apm.oboe.settings import ( + Flags, + LocalSettings, + SampleSource, + Settings, + TracingMode, + merge, +) def test_merge_override_unset(): remote = Settings( sample_rate=1, sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS | Flags.TRIGGERED_TRACE, + flags=Flags.SAMPLE_START + | Flags.SAMPLE_THROUGH_ALWAYS + | Flags.TRIGGERED_TRACE, buckets={}, signature_key=None, timestamp=int(time.time()), - ttl=60 - ) - local = LocalSettings( - tracing_mode=TracingMode.NEVER, - trigger_mode=False + ttl=60, ) + local = LocalSettings(tracing_mode=TracingMode.NEVER, trigger_mode=False) merged = merge(remote, local) assert merged.flags == Flags.OK @@ -36,31 +42,31 @@ def test_merge_override_unset_always_trigger_enabled(): buckets={}, signature_key=None, timestamp=int(time.time()), - ttl=60 - ) - local = LocalSettings( - tracing_mode=TracingMode.ALWAYS, - trigger_mode=True + ttl=60, ) + local = LocalSettings(tracing_mode=TracingMode.ALWAYS, trigger_mode=True) merged = merge(remote, local) - assert merged.flags == (Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS | Flags.TRIGGERED_TRACE) + assert merged.flags == ( + Flags.SAMPLE_START + | Flags.SAMPLE_THROUGH_ALWAYS + | Flags.TRIGGERED_TRACE + ) def test_merge_override_unset_defaults_to_remote(): remote = Settings( sample_rate=1, sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS | Flags.TRIGGERED_TRACE, + flags=Flags.SAMPLE_START + | Flags.SAMPLE_THROUGH_ALWAYS + | Flags.TRIGGERED_TRACE, buckets={}, signature_key=None, timestamp=int(time.time()), - ttl=60 - ) - local = LocalSettings( - trigger_mode=True, - tracing_mode=None + ttl=60, ) + local = LocalSettings(trigger_mode=True, tracing_mode=None) merged = merge(remote, local) assert merged == remote @@ -70,16 +76,16 @@ def test_merge_override_set_never_trigger_disabled(): remote = Settings( sample_rate=1, sample_source=SampleSource.LOCAL_DEFAULT, - flags=Flags.OVERRIDE | Flags.SAMPLE_START | Flags.SAMPLE_THROUGH_ALWAYS | Flags.TRIGGERED_TRACE, + flags=Flags.OVERRIDE + | Flags.SAMPLE_START + | Flags.SAMPLE_THROUGH_ALWAYS + | Flags.TRIGGERED_TRACE, buckets={}, signature_key=None, timestamp=int(time.time()), - ttl=60 - ) - local = LocalSettings( - tracing_mode=TracingMode.NEVER, - trigger_mode=False + ttl=60, ) + local = LocalSettings(tracing_mode=TracingMode.NEVER, trigger_mode=False) merged = merge(remote, local) assert merged.flags == Flags.OVERRIDE @@ -93,12 +99,9 @@ def test_merge_override_set_always_trigger_enabled(): buckets={}, signature_key=None, timestamp=int(time.time()), - ttl=60 - ) - local = LocalSettings( - tracing_mode=TracingMode.ALWAYS, - trigger_mode=True + ttl=60, ) + local = LocalSettings(tracing_mode=TracingMode.ALWAYS, trigger_mode=True) merged = merge(remote, local) assert merged == remote @@ -112,12 +115,9 @@ def test_merge_override_set_defaults_to_remote(): buckets={}, signature_key=None, timestamp=int(time.time()), - ttl=60 - ) - local = LocalSettings( - trigger_mode=False, - tracing_mode=None + ttl=60, ) + local = LocalSettings(trigger_mode=False, tracing_mode=None) merged = merge(remote, local) assert merged == remote diff --git a/tests/unit/test_oboe/test_token_bucket.py b/tests/unit/test_oboe/test_token_bucket.py index a177b515a..b6cba9d77 100644 --- a/tests/unit/test_oboe/test_token_bucket.py +++ b/tests/unit/test_oboe/test_token_bucket.py @@ -133,7 +133,7 @@ def read_properties(): assert len(errors) == 0 assert all(c == 50 for c in results["capacity"]) assert all(r == 5 for r in results["rate"]) - assert all(isinstance(t, (int, float)) for t in results["tokens"]) + assert all(isinstance(t, (int | float)) for t in results["tokens"]) def test_concurrent_update_and_consume(): @@ -203,6 +203,7 @@ def test_fork_reinitializes_lock(): os._exit(0) # Use _exit to avoid cleanup issues except Exception as e: import traceback + error_msg = f"ERROR:{e}:{traceback.format_exc()}\n".encode() os.write(write_fd, error_msg) os.close(write_fd) @@ -224,11 +225,15 @@ def test_fork_reinitializes_lock(): # Verify child exited successfully exit_code = os.WEXITSTATUS(status) if os.WIFEXITED(status) else -1 - assert exit_code == 0, f"Child process failed with exit code {exit_code}, output: {result_str}" + assert exit_code == 0, ( + f"Child process failed with exit code {exit_code}, output: {result_str}" + ) # Parse results parts = result_str.split(",") - assert len(parts) == 3, f"Expected 3 parts but got {len(parts)}: {result_str}" + assert len(parts) == 3, ( + f"Expected 3 parts but got {len(parts)}: {result_str}" + ) result, tokens, child_pid = parts # Verify fork occurred @@ -243,7 +248,9 @@ def test_fork_reinitializes_lock(): # Verify parent still has its state (tokens may have replenished slightly with rate=1) parent_tokens = bucket.tokens - assert 5.0 <= parent_tokens <= 6.0 # Should be close to 5 but may have replenished + assert ( + 5.0 <= parent_tokens <= 6.0 + ) # Should be close to 5 but may have replenished def test_multiple_threads_replenishing_and_consuming(): @@ -275,4 +282,6 @@ def consumer(): # With 10 tokens/sec rate and ~1 second of sleep time, # we should consume initial capacity plus some replenished tokens assert total_consumed[0] >= 50 # At least initial capacity - assert total_consumed[0] <= 100 # But not more than initial + 1 sec replenishment + assert ( + total_consumed[0] <= 100 + ) # But not more than initial + 1 sec replenishment diff --git a/tests/unit/test_oboe/test_trace_options.py b/tests/unit/test_oboe/test_trace_options.py index 11a976c6d..cc12792cf 100644 --- a/tests/unit/test_oboe/test_trace_options.py +++ b/tests/unit/test_oboe/test_trace_options.py @@ -10,101 +10,347 @@ from solarwinds_apm.oboe.trace_options import ( Auth, + TraceOptions, + TraceOptionsResponse, + TriggerTrace, parse_trace_options, stringify_trace_options_response, - TriggerTrace, - validate_signature, TraceOptions, TraceOptionsResponse, + validate_signature, ) -@pytest.mark.parametrize("header, expected", [ - ("=", TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[])), - ("=value", TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[])), - ("trigger-trace", TraceOptions(trigger_trace=True, timestamp=None, sw_keys=None, custom={}, ignored=[])), - ("trigger-trace=value", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[("trigger-trace", "value")])), - ("trigger-trace;trigger-trace", - TraceOptions(trigger_trace=True, timestamp=None, sw_keys=None, custom={}, ignored=[("trigger-trace", None)])), - ("ts", TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[("ts", None)])), - ("ts=1234;ts=5678", - TraceOptions(trigger_trace=None, timestamp=1234, sw_keys=None, custom={}, ignored=[("ts", "5678")])), - ("ts=value", TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[("ts", "value")])), - ("ts=12.34", TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[("ts", "12.34")])), - ("ts = 1234567890 ", TraceOptions(trigger_trace=None, timestamp=1234567890, sw_keys=None, custom={}, ignored=[])), - ("sw-keys", TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[("sw-keys", None)])), - ("sw-keys=keys1;sw-keys=keys2", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys="keys1", custom={}, ignored=[("sw-keys", "keys2")])), - ("sw-keys= name:value ", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys="name:value", custom={}, ignored=[])), - ("sw-keys=check-id:check-1013,website-id;booking-demo", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys="check-id:check-1013,website-id", custom={}, - ignored=[("booking-demo", None)])), - ("custom-key= value ", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={"custom-key": "value"}, ignored=[])), - ("custom-key", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[("custom-key", None)])), - ("custom-key=value1;custom-key=value2", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={"custom-key": "value1"}, - ignored=[("custom-key", "value2")])), - ("custom-key=name=value", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={"custom-key": "name=value"}, ignored=[])), - ("custom- key=value;custom-ke y=value", TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, - ignored=[("custom- key", "value"), ("custom-ke y", "value")])), - ( +@pytest.mark.parametrize( + "header, expected", + [ + ( + "=", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[], + ), + ), + ( + "=value", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[], + ), + ), + ( + "trigger-trace", + TraceOptions( + trigger_trace=True, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[], + ), + ), + ( + "trigger-trace=value", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("trigger-trace", "value")], + ), + ), + ( + "trigger-trace;trigger-trace", + TraceOptions( + trigger_trace=True, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("trigger-trace", None)], + ), + ), + ( + "ts", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("ts", None)], + ), + ), + ( + "ts=1234;ts=5678", + TraceOptions( + trigger_trace=None, + timestamp=1234, + sw_keys=None, + custom={}, + ignored=[("ts", "5678")], + ), + ), + ( + "ts=value", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("ts", "value")], + ), + ), + ( + "ts=12.34", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("ts", "12.34")], + ), + ), + ( + "ts = 1234567890 ", + TraceOptions( + trigger_trace=None, + timestamp=1234567890, + sw_keys=None, + custom={}, + ignored=[], + ), + ), + ( + "sw-keys", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("sw-keys", None)], + ), + ), + ( + "sw-keys=keys1;sw-keys=keys2", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys="keys1", + custom={}, + ignored=[("sw-keys", "keys2")], + ), + ), + ( + "sw-keys= name:value ", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys="name:value", + custom={}, + ignored=[], + ), + ), + ( + "sw-keys=check-id:check-1013,website-id;booking-demo", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys="check-id:check-1013,website-id", + custom={}, + ignored=[("booking-demo", None)], + ), + ), + ( + "custom-key= value ", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={"custom-key": "value"}, + ignored=[], + ), + ), + ( + "custom-key", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("custom-key", None)], + ), + ), + ( + "custom-key=value1;custom-key=value2", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={"custom-key": "value1"}, + ignored=[("custom-key", "value2")], + ), + ), + ( + "custom-key=name=value", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={"custom-key": "name=value"}, + ignored=[], + ), + ), + ( + "custom- key=value;custom-ke y=value", + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("custom- key", "value"), ("custom-ke y", "value")], + ), + ), + ( "key=value", - TraceOptions(trigger_trace=None, timestamp=None, sw_keys=None, custom={}, ignored=[("key", "value")])), - ( + TraceOptions( + trigger_trace=None, + timestamp=None, + sw_keys=None, + custom={}, + ignored=[("key", "value")], + ), + ), + ( "trigger-trace ; custom-something=value; custom-OtherThing = other val ; sw-keys = 029734wr70:9wqj21,0d9j1 ; ts = 12345 ; foo = bar", - TraceOptions(trigger_trace=True, timestamp=12345, sw_keys="029734wr70:9wqj21,0d9j1", - custom={"custom-something": "value", "custom-OtherThing": "other val"}, - ignored=[("foo", "bar")])), - ( + TraceOptions( + trigger_trace=True, + timestamp=12345, + sw_keys="029734wr70:9wqj21,0d9j1", + custom={ + "custom-something": "value", + "custom-OtherThing": "other val", + }, + ignored=[("foo", "bar")], + ), + ), + ( ";foo=bar;;;custom-something=value_thing;;sw-keys=02973r70:1b2a3;;;;custom-key=val;ts=12345;;;;;;;trigger-trace;;;", - TraceOptions(trigger_trace=True, timestamp=12345, sw_keys="02973r70:1b2a3", - custom={"custom-something": "value_thing", "custom-key": "val"}, ignored=[("foo", "bar")])), - ("trigger-trace;custom-foo='bar;bar';custom-bar=foo", - TraceOptions(trigger_trace=True, timestamp=None, sw_keys=None, custom={"custom-foo": "'bar", "custom-bar": "foo"}, - ignored=[("bar'", None)])), - (";trigger-trace;custom-something=value_thing;sw-keys=02973r70:9wqj21,0d9j1;1;2;3;4;5;=custom-key=val?;=", - TraceOptions(trigger_trace=True, timestamp=None, sw_keys="02973r70:9wqj21,0d9j1", - custom={"custom-something": "value_thing"}, - ignored=[("1", None), ("2", None), ("3", None), ("4", None), ("5", None)])), -]) + TraceOptions( + trigger_trace=True, + timestamp=12345, + sw_keys="02973r70:1b2a3", + custom={ + "custom-something": "value_thing", + "custom-key": "val", + }, + ignored=[("foo", "bar")], + ), + ), + ( + "trigger-trace;custom-foo='bar;bar';custom-bar=foo", + TraceOptions( + trigger_trace=True, + timestamp=None, + sw_keys=None, + custom={"custom-foo": "'bar", "custom-bar": "foo"}, + ignored=[("bar'", None)], + ), + ), + ( + ";trigger-trace;custom-something=value_thing;sw-keys=02973r70:9wqj21,0d9j1;1;2;3;4;5;=custom-key=val?;=", + TraceOptions( + trigger_trace=True, + timestamp=None, + sw_keys="02973r70:9wqj21,0d9j1", + custom={"custom-something": "value_thing"}, + ignored=[ + ("1", None), + ("2", None), + ("3", None), + ("4", None), + ("5", None), + ], + ), + ), + ], +) def test_parse_trace_options(header, expected): result = parse_trace_options(header) assert result == expected def test_stringify_trace_options_response(): - result = stringify_trace_options_response(TraceOptionsResponse( - auth=Auth.OK, - trigger_trace=TriggerTrace.OK, - ignored=None, - )) + result = stringify_trace_options_response( + TraceOptionsResponse( + auth=Auth.OK, + trigger_trace=TriggerTrace.OK, + ignored=None, + ) + ) assert result == "auth=ok;trigger-trace=ok" - result = stringify_trace_options_response(TraceOptionsResponse( - auth=Auth.OK, - trigger_trace=TriggerTrace.TRIGGER_TRACING_DISABLED, - ignored=["invalid-key1", "invalid_key2"], - )) - assert result == "auth=ok;trigger-trace=trigger-tracing-disabled;ignored=invalid-key1,invalid_key2" + result = stringify_trace_options_response( + TraceOptionsResponse( + auth=Auth.OK, + trigger_trace=TriggerTrace.TRIGGER_TRACING_DISABLED, + ignored=["invalid-key1", "invalid_key2"], + ) + ) + assert ( + result + == "auth=ok;trigger-trace=trigger-tracing-disabled;ignored=invalid-key1,invalid_key2" + ) -@pytest.mark.parametrize("header, signature, key, timestamp, expected", [ - ("trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", "2c1c398c3e6be898f47f74bf74f035903b48b59c", - "8mZ98ZnZhhggcsUmdMbS", int(time.time()) - 60, Auth.OK), - ("trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", "2c1c398c3e6be898f47f74bf74f035903b48b59d", - "8mZ98ZnZhhggcsUmdMbS", int(time.time()) - 60, Auth.BAD_SIGNATURE), - ("trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", "2c1c398c3e6be898f47f74bf74f035903b48b59c", None, - int(time.time()) - 60, Auth.NO_SIGNATURE_KEY), - ("trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", "2c1c398c3e6be898f47f74bf74f035903b48b59c", - "8mZ98ZnZhhggcsUmdMbS", int(time.time()) - 10 * 60, Auth.BAD_TIMESTAMP), - ("trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", "2c1c398c3e6be898f47f74bf74f035903b48b59c", - "8mZ98ZnZhhggcsUmdMbS", int(time.time()) + 10 * 60, Auth.BAD_TIMESTAMP), - ("trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", "2c1c398c3e6be898f47f74bf74f035903b48b59c", - "8mZ98ZnZhhggcsUmdMbS", None, Auth.BAD_TIMESTAMP), -]) +@pytest.mark.parametrize( + "header, signature, key, timestamp, expected", + [ + ( + "trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", + "2c1c398c3e6be898f47f74bf74f035903b48b59c", + "8mZ98ZnZhhggcsUmdMbS", + int(time.time()) - 60, + Auth.OK, + ), + ( + "trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", + "2c1c398c3e6be898f47f74bf74f035903b48b59d", + "8mZ98ZnZhhggcsUmdMbS", + int(time.time()) - 60, + Auth.BAD_SIGNATURE, + ), + ( + "trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", + "2c1c398c3e6be898f47f74bf74f035903b48b59c", + None, + int(time.time()) - 60, + Auth.NO_SIGNATURE_KEY, + ), + ( + "trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", + "2c1c398c3e6be898f47f74bf74f035903b48b59c", + "8mZ98ZnZhhggcsUmdMbS", + int(time.time()) - 10 * 60, + Auth.BAD_TIMESTAMP, + ), + ( + "trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", + "2c1c398c3e6be898f47f74bf74f035903b48b59c", + "8mZ98ZnZhhggcsUmdMbS", + int(time.time()) + 10 * 60, + Auth.BAD_TIMESTAMP, + ), + ( + "trigger-trace;pd-keys=lo:se,check-id:123;ts=1564597681", + "2c1c398c3e6be898f47f74bf74f035903b48b59c", + "8mZ98ZnZhhggcsUmdMbS", + None, + Auth.BAD_TIMESTAMP, + ), + ], +) def test_validate_signature(header, signature, key, timestamp, expected): result = validate_signature(header, signature, key, timestamp) assert result == expected diff --git a/tests/unit/test_oboe/test_transaction_name_calculator.py b/tests/unit/test_oboe/test_transaction_name_calculator.py index 1e5ce12ae..1e4f19fbb 100644 --- a/tests/unit/test_oboe/test_transaction_name_calculator.py +++ b/tests/unit/test_oboe/test_transaction_name_calculator.py @@ -1,17 +1,55 @@ -from solarwinds_apm.oboe.transaction_name_calculator import resolve_transaction_name +from solarwinds_apm.oboe.transaction_name_calculator import ( + resolve_transaction_name, +) + def test_resolve_transaction_name_unknown(): assert resolve_transaction_name(123) == "unknown" + def test_resolve_transaction_name(): - assert resolve_transaction_name("http://www.boost.org/index.html") == "/index.html" - assert resolve_transaction_name("http://www.boost.org/index.html?field=value") == "/index.html" - assert resolve_transaction_name("http://www.boost.org/index.html?field=value#downloads") == "/index.html" - assert resolve_transaction_name("http://www.boost.org:80/index.html?field=value#downloads") == "/index.html" - assert resolve_transaction_name("https://www.boost.org/index.html") == "/index.html" - assert resolve_transaction_name("https://www.boost.org/index.html?field=value") == "/index.html" - assert resolve_transaction_name("https://www.boost.org/index.html?field=value#downloads") == "/index.html" - assert resolve_transaction_name("https://www.boost.org:80/index.html?field=value#downloads") == "/index.html" + assert ( + resolve_transaction_name("http://www.boost.org/index.html") + == "/index.html" + ) + assert ( + resolve_transaction_name("http://www.boost.org/index.html?field=value") + == "/index.html" + ) + assert ( + resolve_transaction_name( + "http://www.boost.org/index.html?field=value#downloads" + ) + == "/index.html" + ) + assert ( + resolve_transaction_name( + "http://www.boost.org:80/index.html?field=value#downloads" + ) + == "/index.html" + ) + assert ( + resolve_transaction_name("https://www.boost.org/index.html") + == "/index.html" + ) + assert ( + resolve_transaction_name( + "https://www.boost.org/index.html?field=value" + ) + == "/index.html" + ) + assert ( + resolve_transaction_name( + "https://www.boost.org/index.html?field=value#downloads" + ) + == "/index.html" + ) + assert ( + resolve_transaction_name( + "https://www.boost.org:80/index.html?field=value#downloads" + ) + == "/index.html" + ) assert resolve_transaction_name("https://example.com") == "/" assert resolve_transaction_name("https://example.com:8080") == "/" assert resolve_transaction_name("https://example.com/") == "/" @@ -20,15 +58,43 @@ def test_resolve_transaction_name(): assert resolve_transaction_name("ftp://example.com:8000") == "/" assert resolve_transaction_name("sftp://example.com") == "/" assert resolve_transaction_name("sftp://example.com:8000") == "/" - assert resolve_transaction_name("http://www.boost.org/1/2/3/4/5/index.html?field=value#downloads") == "/1/2" - assert resolve_transaction_name("http://www.boost.org:8000/1/2/3/4/5/index.html?field=value#downloads") == "/1/2" - assert resolve_transaction_name("https://user:pass@example.com/1/a.html") == "/1/a.html" - assert resolve_transaction_name("https://@example.com/1/a.html") == "/1/a.html" - assert resolve_transaction_name("https://user@example.com/1/a.html") == "/1/a.html" - assert resolve_transaction_name("https://:pass@example.com/1/a.html") == "/1/a.html" - assert resolve_transaction_name( "https://:@example.com/1/a.html") == "/1/a.html" - assert resolve_transaction_name( "a") == "/a" - assert resolve_transaction_name( "/a") == "/a" - assert resolve_transaction_name( "/a/b") == "/a/b" + assert ( + resolve_transaction_name( + "http://www.boost.org/1/2/3/4/5/index.html?field=value#downloads" + ) + == "/1/2" + ) + assert ( + resolve_transaction_name( + "http://www.boost.org:8000/1/2/3/4/5/index.html?field=value#downloads" + ) + == "/1/2" + ) + assert ( + resolve_transaction_name("https://user:pass@example.com/1/a.html") + == "/1/a.html" + ) + assert ( + resolve_transaction_name("https://@example.com/1/a.html") + == "/1/a.html" + ) + assert ( + resolve_transaction_name("https://user@example.com/1/a.html") + == "/1/a.html" + ) + assert ( + resolve_transaction_name("https://:pass@example.com/1/a.html") + == "/1/a.html" + ) + assert ( + resolve_transaction_name("https://:@example.com/1/a.html") + == "/1/a.html" + ) + assert resolve_transaction_name("a") == "/a" + assert resolve_transaction_name("/a") == "/a" + assert resolve_transaction_name("/a/b") == "/a/b" assert resolve_transaction_name("/") == "/" - assert resolve_transaction_name("images/dot.gif?v=hide#a") == "/images/dot.gif" + assert ( + resolve_transaction_name("images/dot.gif?v=hide#a") + == "/images/dot.gif" + ) diff --git a/tests/unit/test_oboe/test_transaction_name_pool.py b/tests/unit/test_oboe/test_transaction_name_pool.py index ad7e59f8f..25cdcdcf8 100644 --- a/tests/unit/test_oboe/test_transaction_name_pool.py +++ b/tests/unit/test_oboe/test_transaction_name_pool.py @@ -1,11 +1,15 @@ -import pytest import time + +import pytest + from solarwinds_apm.oboe.transaction_name_pool import TransactionNamePool + @pytest.fixture def pool(): return TransactionNamePool() + def test_register_name(pool): name = "test_name" registered_name = pool.registered(name) @@ -13,12 +17,14 @@ def test_register_name(pool): assert name in pool._pool assert len(pool._min_heap) == 1 + def test_register_name_exceeds_max_length(pool): long_name = "a" * (pool._max_length + 10) registered_name = pool.registered(long_name) - assert registered_name == long_name[:pool._max_length] + assert registered_name == long_name[: pool._max_length] assert registered_name in pool._pool + def test_register_name_exceeds_max_size(pool): for i in range(pool._max_size): pool.registered(f"name_{i}") @@ -29,15 +35,17 @@ def test_register_name_exceeds_max_size(pool): registered_name = pool.registered("name_1") assert registered_name == "name_1" + def test_housekeep(pool): name = "test_name" pool.registered(name) - pool._pool[name].timestamp -= (pool._ttl + 1) + pool._pool[name].timestamp -= pool._ttl + 1 pool._housekeep() assert name not in pool._pool assert len(pool._min_heap) == 0 assert len(pool._pool) == 0 + def test_update_timestamp(pool): name = "test_name" pool.registered(name) diff --git a/tests/unit/test_processors/__init__.py b/tests/unit/test_processors/__init__.py index 9167e547e..c3eb85d0c 100644 --- a/tests/unit/test_processors/__init__.py +++ b/tests/unit/test_processors/__init__.py @@ -2,4 +2,4 @@ # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/tests/unit/test_processors/test_response_time_processor.py b/tests/unit/test_processors/test_response_time_processor.py index 18ad81cbb..ef6624748 100644 --- a/tests/unit/test_processors/test_response_time_processor.py +++ b/tests/unit/test_processors/test_response_time_processor.py @@ -6,8 +6,8 @@ from solarwinds_apm.trace import ResponseTimeProcessor -class TestResponseTimeProcessor: +class TestResponseTimeProcessor: def get_mock_apm_config( self, mocker, @@ -15,6 +15,7 @@ def get_mock_apm_config( lambda_function_name="unused", ): mock_apm_config = mocker.Mock() + def outer_side_effect(cnf_key): return outer_txn_retval @@ -22,11 +23,9 @@ def outer_side_effect(cnf_key): side_effect=outer_side_effect, ) mock_apm_config.configure_mock( - **{ - "service_name": "foo-service", - "get": mock_get_outer, - "lambda_function_name": lambda_function_name, - } + service_name="foo-service", + get=mock_get_outer, + lambda_function_name=lambda_function_name, ) return mock_apm_config @@ -47,153 +46,93 @@ def test_is_span_http_true(self, mocker): mock_spankind = mocker.patch( "solarwinds_apm.trace.response_time_processor.SpanKind" ) - mock_spankind.configure_mock( - **{ - "SERVER": "foo" - } - ) + mock_spankind.configure_mock(SERVER="foo") mock_spanattributes = mocker.patch( "solarwinds_apm.trace.response_time_processor.SpanAttributes" ) mock_spanattributes.configure_mock( - **{ - "HTTP_REQUEST_METHOD": "http.request.method" - } + HTTP_REQUEST_METHOD="http.request.method" ) mock_span = mocker.Mock() mock_span.configure_mock( - **{ - "kind": "foo", - "attributes": { - "http.request.method": "bar" - } - } + kind="foo", attributes={"http.request.method": "bar"} ) processor = ResponseTimeProcessor(mocker.Mock()) - assert True == processor.is_span_http(mock_span) + assert processor.is_span_http(mock_span) def test_is_span_http_true_old_attr(self, mocker): mock_spankind = mocker.patch( "solarwinds_apm.trace.response_time_processor.SpanKind" ) - mock_spankind.configure_mock( - **{ - "SERVER": "foo" - } - ) + mock_spankind.configure_mock(SERVER="foo") mock_spanattributes = mocker.patch( "solarwinds_apm.trace.response_time_processor.SpanAttributes" ) - mock_spanattributes.configure_mock( - **{ - "HTTP_METHOD": "http.method" - } - ) + mock_spanattributes.configure_mock(HTTP_METHOD="http.method") mock_span = mocker.Mock() - mock_span.configure_mock( - **{ - "kind": "foo", - "attributes": { - "http.method": "bar" - } - } - ) + mock_span.configure_mock(kind="foo", attributes={"http.method": "bar"}) processor = ResponseTimeProcessor(mocker.Mock()) - assert True == processor.is_span_http(mock_span) + assert processor.is_span_http(mock_span) def test_is_span_http_false_no_http_method(self, mocker): mock_spankind = mocker.patch( "solarwinds_apm.trace.response_time_processor.SpanKind" ) - mock_spankind.configure_mock( - **{ - "SERVER": "foo" - } - ) + mock_spankind.configure_mock(SERVER="foo") mock_spanattributes = mocker.patch( "solarwinds_apm.trace.response_time_processor.SpanAttributes" ) - mock_spanattributes.configure_mock( - **{ - "HTTP_METHOD": "http.method" - } - ) + mock_spanattributes.configure_mock(HTTP_METHOD="http.method") mock_span = mocker.Mock() mock_span.configure_mock( - **{ - "kind": "foo", - "attributes": { - "NOT.http.method.hehehehe": "bar" - } - } + kind="foo", attributes={"NOT.http.method.hehehehe": "bar"} ) processor = ResponseTimeProcessor(mocker.Mock()) - assert False == processor.is_span_http(mock_span) + assert not processor.is_span_http(mock_span) def test_has_error_true(self, mocker): mock_statuscode = mocker.patch( "solarwinds_apm.trace.response_time_processor.StatusCode" ) - mock_statuscode.configure_mock( - **{ - "ERROR": "foo" - } - ) + mock_statuscode.configure_mock(ERROR="foo") mock_span = mocker.Mock() mock_status = mocker.Mock() - mock_status.configure_mock( - **{ - "status_code": "foo" - } - ) - mock_span.configure_mock( - **{ - "status": mock_status - } - ) + mock_status.configure_mock(status_code="foo") + mock_span.configure_mock(status=mock_status) processor = ResponseTimeProcessor(mocker.Mock()) - assert True == processor.has_error(mock_span) + assert processor.has_error(mock_span) def test_has_error_false(self, mocker): mock_statuscode = mocker.patch( "solarwinds_apm.trace.response_time_processor.StatusCode" ) - mock_statuscode.configure_mock( - **{ - "ERROR": "foo" - } - ) + mock_statuscode.configure_mock(ERROR="foo") mock_span = mocker.Mock() mock_status = mocker.Mock() - mock_status.configure_mock( - **{ - "status_code": "not-foo-hehehe" - } - ) - mock_span.configure_mock( - **{ - "status": mock_status - } - ) + mock_status.configure_mock(status_code="not-foo-hehehe") + mock_span.configure_mock(status=mock_status) processor = ResponseTimeProcessor(mocker.Mock()) - assert False == processor.has_error(mock_span) + assert not processor.has_error(mock_span) def test_calculate_span_time_missing(self, mocker): processor = ResponseTimeProcessor(mocker.Mock()) - assert 0 == processor.calculate_span_time(0, 0) - assert 0 == processor.calculate_span_time(0, 1000) - assert 0 == processor.calculate_span_time(1000, 0) + assert processor.calculate_span_time(0, 0) == 0 + assert processor.calculate_span_time(0, 1000) == 0 + assert processor.calculate_span_time(1000, 0) == 0 def test_calculate_span_time_default_1e3(self, mocker): processor = ResponseTimeProcessor(mocker.Mock()) - assert 1 == processor.calculate_span_time(2000, 3000) + assert processor.calculate_span_time(2000, 3000) == 1 def test_calculate_span_time_1e6(self, mocker): processor = ResponseTimeProcessor(mocker.Mock()) - assert 1 == processor.calculate_span_time( - 2000000, - 3000000, - 1e6, + assert ( + processor.calculate_span_time( + 2000000, + 3000000, + 1e6, + ) + == 1 ) def patch_for_on_end( @@ -229,323 +168,351 @@ def patch_for_on_end( mock_set = mocker.Mock() mock_del = mocker.Mock() mock_txname_manager.configure_mock( - **{ - "__setitem__": mock_set, - "__delitem__": mock_del, - "get": mocker.Mock(return_value=get_retval) - } + __setitem__=mock_set, + __delitem__=mock_del, + get=mocker.Mock(return_value=get_retval), ) mock_basic_span = mocker.Mock() if missing_http_attrs: mock_basic_span.configure_mock( - **{ - "attributes": { - "sw.transaction": "foo" - }, - } + attributes={"sw.transaction": "foo"} ) else: mock_basic_span.configure_mock( - **{ - "attributes": { - "http.request.method": "foo-method", - "http.response.status_code": 200, - "sw.transaction": "foo" - }, + attributes={ + "http.request.method": "foo-method", + "http.response.status_code": 200, + "sw.transaction": "foo", } ) - mock_get_meter = mocker.patch( "solarwinds_apm.trace.response_time_processor.get_meter" ) mock_meter = mocker.Mock() mock_get_meter.return_value = mock_meter - mock_histogram= mocker.Mock() + mock_histogram = mocker.Mock() mock_meter.create_histogram.return_value = mock_histogram mock_histogram.record = mocker.Mock() - return mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span + return ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) def test_enhance_meter_attrs_with_http_span_attrs_new_attrs(self, mocker): mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "GET", - "http.response.status_code": 200 + "http.response.status_code": 200, } - }) - + ) + meter_attrs = {"sw.is_error": False} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 200 assert result["http.request.method"] == "GET" - assert result["sw.is_error"] == False + assert not result["sw.is_error"] def test_enhance_meter_attrs_with_http_span_attrs_old_attrs(self, mocker): mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { - "http.method": "POST", - "http.status_code": 404 - } - }) - + mock_span.configure_mock( + attributes={"http.method": "POST", "http.status_code": 404} + ) + meter_attrs = {"sw.is_error": True} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 404 assert result["http.request.method"] == "POST" - assert result["sw.is_error"] == True + assert result["sw.is_error"] - def test_enhance_meter_attrs_with_http_span_attrs_new_preferred_over_old(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_new_preferred_over_old( + self, mocker + ): mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "GET", "http.method": "POST", "http.response.status_code": 200, - "http.status_code": 500 + "http.status_code": 500, } - }) - + ) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 200 assert result["http.request.method"] == "GET" - def test_enhance_meter_attrs_with_http_span_attrs_zero_status_code_fallback(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_zero_status_code_fallback( + self, mocker + ): mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "PUT", "http.response.status_code": 0, - "http.status_code": 201 + "http.status_code": 201, } - }) - + ) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 201 assert result["http.request.method"] == "PUT" - def test_enhance_meter_attrs_with_http_span_attrs_no_status_code(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_no_status_code( + self, mocker + ): mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { - "http.request.method": "DELETE" - } - }) - + mock_span.configure_mock(attributes={"http.request.method": "DELETE"}) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 0 assert result["http.request.method"] == "DELETE" def test_enhance_meter_attrs_with_http_span_attrs_no_method(self, mocker): mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { - "http.response.status_code": 302 - } - }) - + mock_span.configure_mock(attributes={"http.response.status_code": 302}) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 302 assert "http.request.method" not in result - def test_enhance_meter_attrs_with_http_span_attrs_no_http_attrs(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_no_http_attrs( + self, mocker + ): mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { - "some.other.attr": "value" - } - }) - + mock_span.configure_mock(attributes={"some.other.attr": "value"}) + meter_attrs = {"existing": "value"} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 0 assert "http.request.method" not in result assert result["existing"] == "value" - def test_enhance_meter_attrs_with_http_span_attrs_string_status_code_new_attr(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_string_status_code_new_attr( + self, mocker + ): """Test that string status codes are converted to int (new attr)""" mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "GET", - "http.response.status_code": "200" # String instead of int + "http.response.status_code": "200", # String instead of int } - }) - + ) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 200 assert result["http.request.method"] == "GET" - def test_enhance_meter_attrs_with_http_span_attrs_string_status_code_old_attr(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_string_status_code_old_attr( + self, mocker + ): """Test that string status codes are converted to int (deprecated attr)""" mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.method": "POST", - "http.status_code": "404" # String instead of int + "http.status_code": "404", # String instead of int } - }) - + ) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + assert result["http.response.status_code"] == 404 assert result["http.request.method"] == "POST" - def test_enhance_meter_attrs_with_http_span_attrs_invalid_string_status_code(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_invalid_string_status_code( + self, mocker + ): """Test that invalid string status codes fall back to unavailable (0)""" mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "GET", - "http.response.status_code": "invalid" # Cannot convert to int + "http.response.status_code": "invalid", # Cannot convert to int } - }) - + ) + meter_attrs = {} - mock_logger = mocker.patch("solarwinds_apm.trace.response_time_processor.logger") - - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - - assert result["http.response.status_code"] == 0 # Fallback to unavailable + mock_logger = mocker.patch( + "solarwinds_apm.trace.response_time_processor.logger" + ) + + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + + assert ( + result["http.response.status_code"] == 0 + ) # Fallback to unavailable assert result["http.request.method"] == "GET" mock_logger.debug.assert_called_once() - assert "Expected HTTP status code as int" in mock_logger.debug.call_args[0][0] + assert ( + "Expected HTTP status code as int" + in mock_logger.debug.call_args[0][0] + ) - def test_enhance_meter_attrs_with_http_span_attrs_invalid_type_status_code(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_invalid_type_status_code( + self, mocker + ): """Test that non-string/non-int status codes fall back to unavailable (0)""" mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "GET", - "http.response.status_code": ["200"] # List instead of int + "http.response.status_code": ["200"], # List instead of int } - }) - + ) + meter_attrs = {} - mock_logger = mocker.patch("solarwinds_apm.trace.response_time_processor.logger") - - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - - assert result["http.response.status_code"] == 0 # Fallback to unavailable + mock_logger = mocker.patch( + "solarwinds_apm.trace.response_time_processor.logger" + ) + + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + + assert ( + result["http.response.status_code"] == 0 + ) # Fallback to unavailable assert result["http.request.method"] == "GET" mock_logger.debug.assert_called_once() - assert "Expected HTTP status code as int" in mock_logger.debug.call_args[0][0] + assert ( + "Expected HTTP status code as int" + in mock_logger.debug.call_args[0][0] + ) - def test_enhance_meter_attrs_with_http_span_attrs_string_zero_status_code(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_string_zero_status_code( + self, mocker + ): """Test that string '0' is converted but treated as invalid (fallback to old attr)""" mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "GET", "http.response.status_code": "0", - "http.status_code": 200 + "http.status_code": 200, } - }) - + ) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + # String "0" converts to int 0, which is <= 0, so falls back to old attr assert result["http.response.status_code"] == 200 assert result["http.request.method"] == "GET" - def test_enhance_meter_attrs_with_http_span_attrs_negative_string_status_code(self, mocker): + def test_enhance_meter_attrs_with_http_span_attrs_negative_string_status_code( + self, mocker + ): """Test that negative string status codes are treated as invalid""" mock_apm_config = self.get_mock_apm_config(mocker) processor = ResponseTimeProcessor(mock_apm_config) - + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "attributes": { + mock_span.configure_mock( + attributes={ "http.request.method": "GET", - "http.response.status_code": "-1" + "http.response.status_code": "-1", } - }) - + ) + meter_attrs = {} - result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) - + result = processor.enhance_meter_attrs_with_http_span_attrs( + mock_span, meter_attrs + ) + # Negative values are treated as invalid, fallback to unavailable assert result["http.response.status_code"] == 0 assert result["http.request.method"] == "GET" def test_on_end_valid_local_parent_span(self, mocker): """Only scenario to skip OTLP metrics generation (not entry span)""" - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, ) + ) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": True, - "is_remote": False, - } - ) - mock_span.configure_mock( - **{ - "parent": mock_parent - } - ) + mock_parent.configure_mock(is_valid=True, is_remote=False) + mock_span.configure_mock(parent=mock_parent) processor = ResponseTimeProcessor( mock_apm_config, @@ -555,29 +522,21 @@ def test_on_end_valid_local_parent_span(self, mocker): mock_histogram.record.assert_not_called() def test_on_end_valid_remote_parent_span(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, ) + ) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": True, - "is_remote": True, - } - ) + mock_parent.configure_mock(is_valid=True, is_remote=True) mock_span.configure_mock( - **{ - "parent": mock_parent, - "attributes": { - "http.method": "foo-method", - "http.status_code": 200, - "sw.transaction": "foo" - } - } + parent=mock_parent, + attributes={ + "http.method": "foo-method", + "http.status_code": 200, + "sw.transaction": "foo", + }, ) processor = ResponseTimeProcessor( @@ -591,39 +550,31 @@ def test_on_end_valid_remote_parent_span(self, mocker): mocker.call( amount=123, attributes={ - 'sw.is_error': True, - 'http.response.status_code': 200, - 'http.request.method': 'foo-method', - 'sw.transaction': 'foo' - } + "sw.is_error": True, + "http.response.status_code": 200, + "http.request.method": "foo-method", + "sw.transaction": "foo", + }, ) ] ) def test_on_end_invalid_remote_parent_span(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, ) + ) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": False, - "is_remote": True, - } - ) + mock_parent.configure_mock(is_valid=False, is_remote=True) mock_span.configure_mock( - **{ - "parent": mock_parent, - "attributes": { - "http.method": "foo-method", - "http.status_code": 200, - "sw.transaction": "foo" - } - } + parent=mock_parent, + attributes={ + "http.method": "foo-method", + "http.status_code": 200, + "sw.transaction": "foo", + }, ) processor = ResponseTimeProcessor( @@ -637,39 +588,31 @@ def test_on_end_invalid_remote_parent_span(self, mocker): mocker.call( amount=123, attributes={ - 'sw.is_error': True, - 'http.response.status_code': 200, - 'http.request.method': 'foo-method', - 'sw.transaction': 'foo' - } + "sw.is_error": True, + "http.response.status_code": 200, + "http.request.method": "foo-method", + "sw.transaction": "foo", + }, ) ] ) def test_on_end_invalid_local_parent_span(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, ) + ) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": False, - "is_remote": False, - } - ) + mock_parent.configure_mock(is_valid=False, is_remote=False) mock_span.configure_mock( - **{ - "parent": mock_parent, - "attributes": { - "http.method": "foo-method", - "http.response.status_code": 200, - "sw.transaction": "foo" - } - } + parent=mock_parent, + attributes={ + "http.method": "foo-method", + "http.response.status_code": 200, + "sw.transaction": "foo", + }, ) processor = ResponseTimeProcessor( @@ -683,32 +626,29 @@ def test_on_end_invalid_local_parent_span(self, mocker): mocker.call( amount=123, attributes={ - 'sw.is_error': True, - 'http.response.status_code': 200, - 'http.request.method': 'foo-method', - 'sw.transaction': 'foo' - } + "sw.is_error": True, + "http.response.status_code": 200, + "http.request.method": "foo-method", + "sw.transaction": "foo", + }, ) ] ) def test_on_end_missing_parent(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, ) + ) mock_span = mocker.Mock() mock_span.configure_mock( - **{ - "parent": None, - "attributes": { - "http.method": "foo-method", - "http.response.status_code": 200, - "sw.transaction": "foo" - } - } + parent=None, + attributes={ + "http.method": "foo-method", + "http.response.status_code": 200, + "sw.transaction": "foo", + }, ) processor = ResponseTimeProcessor( @@ -722,36 +662,40 @@ def test_on_end_missing_parent(self, mocker): mocker.call( amount=123, attributes={ - 'sw.is_error': True, - 'http.response.status_code': 200, - 'http.request.method': 'foo-method', - 'sw.transaction': 'foo' - } + "sw.is_error": True, + "http.response.status_code": 200, + "http.request.method": "foo-method", + "sw.transaction": "foo", + }, ) ] ) def test_on_end_missing_txn_name(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - get_retval=None, - ) + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + get_retval=None, + ) processor = ResponseTimeProcessor( mock_apm_config, ) processor.on_end(mock_basic_span) def test_on_end_txn_name_wrong_type(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - get_retval="some-str", - ) + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + get_retval="some-str", + ) processor = ResponseTimeProcessor( mock_apm_config, @@ -759,15 +703,17 @@ def test_on_end_txn_name_wrong_type(self, mocker): processor.on_end(mock_basic_span) def test_on_end_is_span_http_has_error(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - has_error=True, - is_span_http=True, - ) - + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + has_error=True, + is_span_http=True, + ) + processor = ResponseTimeProcessor( mock_apm_config, ) @@ -779,25 +725,27 @@ def test_on_end_is_span_http_has_error(self, mocker): mocker.call( amount=123, attributes={ - 'sw.is_error': True, - 'http.response.status_code': 200, - 'http.request.method': 'foo-method', - 'sw.transaction': 'foo' - } + "sw.is_error": True, + "http.response.status_code": 200, + "http.request.method": "foo-method", + "sw.transaction": "foo", + }, ) ] ) def test_on_end_is_span_http_not_has_error(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - has_error=False, - is_span_http=True, - ) - + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + has_error=False, + is_span_http=True, + ) + processor = ResponseTimeProcessor( mock_apm_config, ) @@ -809,25 +757,27 @@ def test_on_end_is_span_http_not_has_error(self, mocker): mocker.call( amount=123, attributes={ - 'sw.is_error': False, - 'http.response.status_code': 200, - 'http.request.method': 'foo-method', - 'sw.transaction': 'foo' - } + "sw.is_error": False, + "http.response.status_code": 200, + "http.request.method": "foo-method", + "sw.transaction": "foo", + }, ) ] ) def test_on_end_is_span_http_no_status_code_no_method(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - has_error=True, - is_span_http=True, - missing_http_attrs=True, - ) + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + has_error=True, + is_span_http=True, + missing_http_attrs=True, + ) processor = ResponseTimeProcessor( mock_apm_config, @@ -840,24 +790,26 @@ def test_on_end_is_span_http_no_status_code_no_method(self, mocker): mocker.call( amount=123, attributes={ - 'sw.is_error': True, - 'http.response.status_code': 0, - 'sw.transaction': 'foo' - } + "sw.is_error": True, + "http.response.status_code": 0, + "sw.transaction": "foo", + }, ) ] ) def test_on_end_not_is_span_http_has_error(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - has_error=True, - is_span_http=False, - ) - + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + has_error=True, + is_span_http=False, + ) + processor = ResponseTimeProcessor( mock_apm_config, ) @@ -868,24 +820,23 @@ def test_on_end_not_is_span_http_has_error(self, mocker): [ mocker.call( amount=123, - attributes={ - 'sw.is_error': True, - 'sw.transaction': 'foo' - } + attributes={"sw.is_error": True, "sw.transaction": "foo"}, ) ] ) def test_on_end_not_is_span_http_not_has_error(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - has_error=False, - is_span_http=False, - ) - + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + has_error=False, + is_span_http=False, + ) + processor = ResponseTimeProcessor( mock_apm_config, ) @@ -896,199 +847,192 @@ def test_on_end_not_is_span_http_not_has_error(self, mocker): [ mocker.call( amount=123, - attributes={ - 'sw.is_error': False, - 'sw.transaction': 'foo' - } + attributes={"sw.is_error": False, "sw.transaction": "foo"}, ) ] ) def test_on_end_http_new_status_code_attr(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, has_error=False, is_span_http=True, ) - + ) + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "parent": None, - "attributes": { + mock_span.configure_mock( + parent=None, + attributes={ "http.request.method": "GET", "http.response.status_code": 200, - "sw.transaction": "test-transaction" - } - }) - + "sw.transaction": "test-transaction", + }, + ) + processor = ResponseTimeProcessor(mock_apm_config) processor.on_end(mock_span) - + mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args - assert call_args[1]['attributes']['http.response.status_code'] == 200 - assert call_args[1]['attributes']['http.request.method'] == "GET" + assert call_args[1]["attributes"]["http.response.status_code"] == 200 + assert call_args[1]["attributes"]["http.request.method"] == "GET" def test_on_end_http_old_status_code_attr(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, has_error=False, is_span_http=True, ) - + ) + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "parent": None, - "attributes": { + mock_span.configure_mock( + parent=None, + attributes={ "http.method": "POST", "http.status_code": 404, - "sw.transaction": "test-transaction" - } - }) - + "sw.transaction": "test-transaction", + }, + ) + processor = ResponseTimeProcessor(mock_apm_config) processor.on_end(mock_span) - + mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args - assert call_args[1]['attributes']['http.response.status_code'] == 404 - assert call_args[1]['attributes']['http.request.method'] == "POST" + assert call_args[1]["attributes"]["http.response.status_code"] == 404 + assert call_args[1]["attributes"]["http.request.method"] == "POST" def test_on_end_http_new_attrs_preferred_over_old(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, has_error=False, is_span_http=True, ) - + ) + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "parent": None, - "attributes": { + mock_span.configure_mock( + parent=None, + attributes={ "http.request.method": "GET", # new "http.method": "POST", # old (should be ignored) "http.response.status_code": 200, # new "http.status_code": 500, # old (should be ignored) - "sw.transaction": "test-transaction" - } - }) - + "sw.transaction": "test-transaction", + }, + ) + processor = ResponseTimeProcessor(mock_apm_config) processor.on_end(mock_span) - + mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args # Should use new attributes, not old ones - assert call_args[1]['attributes']['http.response.status_code'] == 200 - assert call_args[1]['attributes']['http.request.method'] == "GET" + assert call_args[1]["attributes"]["http.response.status_code"] == 200 + assert call_args[1]["attributes"]["http.request.method"] == "GET" def test_on_end_http_zero_status_code_fallback(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, has_error=False, is_span_http=True, ) - + ) + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "parent": None, - "attributes": { + mock_span.configure_mock( + parent=None, + attributes={ "http.request.method": "GET", "http.response.status_code": 0, # invalid "http.status_code": 201, # should be used - "sw.transaction": "test-transaction" - } - }) - + "sw.transaction": "test-transaction", + }, + ) + processor = ResponseTimeProcessor(mock_apm_config) processor.on_end(mock_span) - + mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args - assert call_args[1]['attributes']['http.response.status_code'] == 201 + assert call_args[1]["attributes"]["http.response.status_code"] == 201 def test_on_end_http_no_status_code_unavailable(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, has_error=False, is_span_http=True, ) - + ) + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "parent": None, - "attributes": { + mock_span.configure_mock( + parent=None, + attributes={ "http.request.method": "GET", - "sw.transaction": "test-transaction" - } - }) - + "sw.transaction": "test-transaction", + }, + ) + processor = ResponseTimeProcessor(mock_apm_config) processor.on_end(mock_span) - + mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args - assert call_args[1]['attributes']['http.response.status_code'] == 0 + assert call_args[1]["attributes"]["http.response.status_code"] == 0 def test_on_end_http_fallback_to_old_method(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - _ = self.patch_for_on_end( + mock_txname_manager, mock_apm_config, mock_histogram, _ = ( + self.patch_for_on_end( mocker, has_error=False, is_span_http=True, ) - + ) + mock_span = mocker.Mock() - mock_span.configure_mock(**{ - "parent": None, - "attributes": { + mock_span.configure_mock( + parent=None, + attributes={ "http.method": "DELETE", # old attribute "http.response.status_code": 204, - "sw.transaction": "test-transaction" - } - }) - + "sw.transaction": "test-transaction", + }, + ) + processor = ResponseTimeProcessor(mock_apm_config) processor.on_end(mock_span) - + mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args - assert call_args[1]['attributes']['http.request.method'] == "DELETE" + assert call_args[1]["attributes"]["http.request.method"] == "DELETE" def test_on_end_non_http_span_no_http_attrs(self, mocker): - mock_txname_manager, \ - mock_apm_config, \ - mock_histogram, \ - mock_basic_span = self.patch_for_on_end( - mocker, - has_error=False, - is_span_http=False, - missing_http_attrs=True, - ) - + ( + mock_txname_manager, + mock_apm_config, + mock_histogram, + mock_basic_span, + ) = self.patch_for_on_end( + mocker, + has_error=False, + is_span_http=False, + missing_http_attrs=True, + ) + processor = ResponseTimeProcessor(mock_apm_config) processor.on_end(mock_basic_span) - + mock_histogram.record.assert_called_once() call_args = mock_histogram.record.call_args # Should not contain HTTP attributes - assert 'http.response.status_code' not in call_args[1]['attributes'] - assert 'http.request.method' not in call_args[1]['attributes'] - assert call_args[1]['attributes']['sw.transaction'] == "foo" + assert "http.response.status_code" not in call_args[1]["attributes"] + assert "http.request.method" not in call_args[1]["attributes"] + assert call_args[1]["attributes"]["sw.transaction"] == "foo" diff --git a/tests/unit/test_processors/test_serviceentry_processor.py b/tests/unit/test_processors/test_serviceentry_processor.py index 3885ed721..a8a97a701 100644 --- a/tests/unit/test_processors/test_serviceentry_processor.py +++ b/tests/unit/test_processors/test_serviceentry_processor.py @@ -12,16 +12,12 @@ ) from solarwinds_apm.trace import ServiceEntrySpanProcessor -class TestServiceEntrySpanProcessor(): +class TestServiceEntrySpanProcessor: def patch_for_on_start(self, mocker): mock_pool = mocker.Mock() mock_registered = mocker.Mock(return_value="mock-registered-name") - mock_pool.configure_mock( - **{ - "registered": mock_registered, - } - ) + mock_pool.configure_mock(registered=mock_registered) mock_get_transaction_name_pool = mocker.patch( "solarwinds_apm.trace.serviceentry_processor.get_transaction_name_pool", return_value=mock_pool, @@ -34,21 +30,13 @@ def patch_for_on_start(self, mocker): mock_set_value = mocker.Mock() mock_set_value.return_value = "foo-set-return" mock_otel_context.configure_mock( - **{ - "attach": mock_attach, - "detach": mock_detach, - "set_value": mock_set_value, - } + attach=mock_attach, detach=mock_detach, set_value=mock_set_value ) mock_w3c = mocker.patch( "solarwinds_apm.trace.serviceentry_processor.W3CTransformer" ) mock_ts_id = mocker.Mock(return_value="some-id") - mock_w3c.configure_mock( - **{ - "trace_and_span_id_from_context": mock_ts_id - } - ) + mock_w3c.configure_mock(trace_and_span_id_from_context=mock_ts_id) return mock_get_transaction_name_pool, mock_otel_context def test_on_start_valid_local_parent_span(self, mocker): @@ -56,12 +44,7 @@ def test_on_start_valid_local_parent_span(self, mocker): mock_pool, mock_context = self.patch_for_on_start(mocker) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": True, - "is_remote": False, - } - ) + mock_parent.configure_mock(is_valid=True, is_remote=False) mock_attrs_get = mocker.Mock(return_value=None) mock_span.configure_mock( **{ @@ -80,12 +63,7 @@ def test_on_start_valid_remote_parent_span(self, mocker): mock_pool, mock_context = self.patch_for_on_start(mocker) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": True, - "is_remote": True, - } - ) + mock_parent.configure_mock(is_valid=True, is_remote=True) mock_attrs_get = mocker.Mock(return_value=None) mock_span.configure_mock( **{ @@ -100,7 +78,7 @@ def test_on_start_valid_remote_parent_span(self, mocker): [ mocker.call("faas.name", None), mocker.call("http.route", None), - mocker.call("url.path", None) + mocker.call("url.path", None), ] ) mock_context.set_value.assert_called_once_with( @@ -115,12 +93,7 @@ def test_on_start_invalid_remote_parent_span(self, mocker): mock_pool, mock_context = self.patch_for_on_start(mocker) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": False, - "is_remote": True, - } - ) + mock_parent.configure_mock(is_valid=False, is_remote=True) mock_attrs_get = mocker.Mock(return_value=None) mock_span.configure_mock( **{ @@ -135,7 +108,7 @@ def test_on_start_invalid_remote_parent_span(self, mocker): [ mocker.call("faas.name", None), mocker.call("http.route", None), - mocker.call("url.path", None) + mocker.call("url.path", None), ] ) mock_context.set_value.assert_called_once_with( @@ -150,12 +123,7 @@ def test_on_start_invalid_local_parent_span(self, mocker): mock_pool, mock_context = self.patch_for_on_start(mocker) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": False, - "is_remote": False, - } - ) + mock_parent.configure_mock(is_valid=False, is_remote=False) mock_attrs_get = mocker.Mock(return_value=None) mock_span.configure_mock( **{ @@ -170,7 +138,7 @@ def test_on_start_invalid_local_parent_span(self, mocker): [ mocker.call("faas.name", None), mocker.call("http.route", None), - mocker.call("url.path", None) + mocker.call("url.path", None), ] ) mock_context.set_value.assert_called_once_with( @@ -198,7 +166,7 @@ def test_on_start_missing_parent(self, mocker): [ mocker.call("faas.name", None), mocker.call("http.route", None), - mocker.call("url.path", None) + mocker.call("url.path", None), ] ) mock_context.set_value.assert_called_once_with( @@ -217,11 +185,11 @@ def test_on_start_sw_apm_transaction_name(self, mocker): ) mock_span = mocker.Mock() mock_span.configure_mock( - **{ - "attributes.get": mocker.Mock(return_value=None) - } + **{"attributes.get": mocker.Mock(return_value=None)} + ) + mocker.patch.dict( + os.environ, {"SW_APM_TRANSACTION_NAME": "sw-apm-transaction"} ) - mocker.patch.dict(os.environ, {"SW_APM_TRANSACTION_NAME": "sw-apm-transaction"}) processor = ServiceEntrySpanProcessor() processor.set_default_transaction_name = mocker.Mock() processor.on_start(mock_span, None) @@ -239,7 +207,9 @@ def test_on_start_faas_name(self, mocker): mock_span.configure_mock( **{ "attributes.get": mocker.Mock( - side_effect=lambda key, default=None: "faas-value" if key == "faas.name" else default + side_effect=lambda key, default=None: "faas-value" + if key == "faas.name" + else default ) } ) @@ -258,11 +228,11 @@ def test_on_start_lambda_function_name(self, mocker): ) mock_span = mocker.Mock() mock_span.configure_mock( - **{ - "attributes.get": mocker.Mock(return_value=None) - } + **{"attributes.get": mocker.Mock(return_value=None)} + ) + mocker.patch.dict( + os.environ, {"AWS_LAMBDA_FUNCTION_NAME": "lambda-function"} ) - mocker.patch.dict(os.environ, {"AWS_LAMBDA_FUNCTION_NAME": "lambda-function"}) processor = ServiceEntrySpanProcessor() processor.set_default_transaction_name = mocker.Mock() processor.on_start(mock_span, None) @@ -280,7 +250,9 @@ def test_on_start_http_route(self, mocker): mock_span.configure_mock( **{ "attributes.get": mocker.Mock( - side_effect=lambda key, default=None: "http-route" if key == "http.route" else default + side_effect=lambda key, default=None: "http-route" + if key == "http.route" + else default ) } ) @@ -301,7 +273,9 @@ def test_on_start_url_path(self, mocker): mock_span.configure_mock( **{ "attributes.get": mocker.Mock( - side_effect=lambda key, default=None: "url-path" if key == "url.path" else default + side_effect=lambda key, default=None: "url-path" + if key == "url.path" + else default ) } ) @@ -336,12 +310,7 @@ def test_on_end_valid_local_parent_span(self, mocker): _, mock_context = self.patch_for_on_start(mocker) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": True, - "is_remote": False, - } - ) + mock_parent.configure_mock(is_valid=True, is_remote=False) mock_attrs_get = mocker.Mock(return_value=None) mock_span.configure_mock( **{ @@ -357,12 +326,7 @@ def test_on_end_valid_remote_parent_span(self, mocker): _, mock_context = self.patch_for_on_start(mocker) mock_span = mocker.Mock() mock_parent = mocker.Mock() - mock_parent.configure_mock( - **{ - "is_valid": True, - "is_remote": True, - } - ) + mock_parent.configure_mock(is_valid=True, is_remote=True) mock_attrs_get = mocker.Mock(return_value=None) mock_span.configure_mock( **{ diff --git a/tests/unit/test_propagator.py b/tests/unit/test_propagator.py index ce69a2eb8..2c12c69b2 100644 --- a/tests/unit/test_propagator.py +++ b/tests/unit/test_propagator.py @@ -14,17 +14,16 @@ from solarwinds_apm.propagator import SolarWindsPropagator -class TestSolarWindsPropagator(): - +class TestSolarWindsPropagator: def test_extract_new_context_no_xtraceoptions(self): - mock_carrier = dict() + mock_carrier = {} result = SolarWindsPropagator().extract(mock_carrier) assert isinstance(result, Context) def test_extract_new_context_xtraceoptions_and_signature(self): mock_carrier = { "x-trace-options": "foo", - "x-trace-options-signature": "bar" + "x-trace-options-signature": "bar", } result = SolarWindsPropagator().extract(mock_carrier) actual_xto = result.get(INTL_SWO_X_OPTIONS_KEY) @@ -32,9 +31,7 @@ def test_extract_new_context_xtraceoptions_and_signature(self): assert actual_xto.signature == "bar" def test_extract_new_context_no_xtraceoptions_yes_signature(self): - mock_carrier = { - "x-trace-options-signature": "bar" - } + mock_carrier = {"x-trace-options-signature": "bar"} result = SolarWindsPropagator().extract(mock_carrier) actual_xto = result.get(INTL_SWO_X_OPTIONS_KEY) assert actual_xto.options_header == "" @@ -43,13 +40,15 @@ def test_extract_new_context_no_xtraceoptions_yes_signature(self): def test_extract_existing_context(self): mock_carrier = { "x-trace-options": "foo", - "x-trace-options-signature": "bar" + "x-trace-options-signature": "bar", } mock_otel_context = { "foo_key": "foo_value", "sw_xtraceoptions": "dont_know_why_this_here_but_will_be_replaced", } - result = SolarWindsPropagator().extract(mock_carrier, mock_otel_context) + result = SolarWindsPropagator().extract( + mock_carrier, mock_otel_context + ) # This one is kept as-is assert result.get("foo_key") == "foo_value" @@ -58,7 +57,9 @@ def test_extract_existing_context(self): assert actual_xto.options_header == "foo" assert actual_xto.signature == "bar" - def mock_otel_context(self, mocker, valid_span_id=True, trace_flags=0x01, trace_state=None): + def mock_otel_context( + self, mocker, valid_span_id=True, trace_flags=0x01, trace_state=None + ): """Shared mocks for OTel trace context Parameters: @@ -71,34 +72,28 @@ def mock_otel_context(self, mocker, valid_span_id=True, trace_flags=0x01, trace_ mock_get_span_context = mocker.Mock() if valid_span_id: mock_get_span_context.configure_mock( - **{ - "trace_id": 0x3A02F8A392478C3700000000DEADBEEF, - "span_id": 0x1000100010001000, - "trace_flags": trace_flags, - "trace_state": trace_state, - "is_valid": True, - } + trace_id=0x3A02F8A392478C3700000000DEADBEEF, + span_id=0x1000100010001000, + trace_flags=trace_flags, + trace_state=trace_state, + is_valid=True, ) else: mock_get_span_context.configure_mock( - **{ - "trace_id": 0x0000000000000000000000000000000, - "span_id": 0x0000000000000000, - "trace_flags": 0x00, - "trace_state": trace_state, - "is_valid": False, - } + trace_id=0x0000000000000000000000000000000, + span_id=0x0000000000000000, + trace_flags=0x00, + trace_state=trace_state, + is_valid=False, ) - mock_get_span_context.is_valid = bool(valid_span_id) and mock_get_span_context.trace_id != 0 + mock_get_span_context.is_valid = ( + bool(valid_span_id) and mock_get_span_context.trace_id != 0 + ) mock_get_current_span = mocker.Mock() mock_get_current_span.configure_mock( - **{ - "get_span_context.return_value": mock_get_span_context - } - ) - mock_trace = mocker.patch( - "solarwinds_apm.propagator.trace" + **{"get_span_context.return_value": mock_get_span_context} ) + mock_trace = mocker.patch("solarwinds_apm.propagator.trace") # Create a unique INVALID_SPAN_CONTEXT sentinel that won't match valid mocks mock_invalid_span_context = mocker.Mock() mock_trace.configure_mock( @@ -111,15 +106,11 @@ def mock_otel_context(self, mocker, valid_span_id=True, trace_flags=0x01, trace_ def test_inject_no_tracestate_invalid_span_id(self, mocker): """No tracestate set in carrier""" self.mock_otel_context(mocker, False) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, @@ -131,256 +122,264 @@ def test_inject_no_tracestate_invalid_span_id(self, mocker): def test_inject_no_tracestate_new_tracestate(self, mocker): """New tracestate with sw added to start""" self.mock_otel_context(mocker, True) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) # OTel context mocked with span_id 0x1000100010001000, trace_flags 0x01 - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-01")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", + ), + call( + mock_carrier, + "tracestate", + TraceState([("sw", "1000100010001000-01")]).to_header(), + ), + ] + ) def test_inject_no_tracestate_new_tracestate_random_not_sampled_flag( self, mocker ): """New tracestate preserves non-sampled random flag 02""" self.mock_otel_context(mocker, True, trace_flags=0x02) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-02", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-02")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-02", + ), + call( + mock_carrier, + "tracestate", + TraceState([("sw", "1000100010001000-02")]).to_header(), + ), + ] + ) def test_inject_no_tracestate_new_tracestate_random_sampled_flag( self, mocker ): """New tracestate preserves sampled random flag 03""" self.mock_otel_context(mocker, True, trace_flags=0x03) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-03", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-03")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-03", + ), + call( + mock_carrier, + "tracestate", + TraceState([("sw", "1000100010001000-03")]).to_header(), + ), + ] + ) def test_inject_existing_tracestate_no_sw(self, mocker): """sw added to start, foo=bar kept, xtrace_options_response removed""" - trace_state = TraceState([("xtrace_options_response", "abc123"), ("foo", "bar")]) + trace_state = TraceState( + [("xtrace_options_response", "abc123"), ("foo", "bar")] + ) self.mock_otel_context(mocker, True, trace_state=trace_state) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) # OTel context mocked with span_id 0x1000100010001000, trace_flags 0x01 - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-01"), ("foo", "bar")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", + ), + call( + mock_carrier, + "tracestate", + TraceState( + [("sw", "1000100010001000-01"), ("foo", "bar")] + ).to_header(), + ), + ] + ) def test_inject_existing_tracestate_existing_sw(self, mocker): """sw updated and moved to start, foo=bar kept, xtrace_options_response removed""" - trace_state = TraceState([("xtrace_options_response", "abc123"), ("foo", "bar"), ("sw", "some-existing-value")]) + trace_state = TraceState( + [ + ("xtrace_options_response", "abc123"), + ("foo", "bar"), + ("sw", "some-existing-value"), + ] + ) self.mock_otel_context(mocker, True, trace_state=trace_state) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) # OTel context mocked with span_id 0x1000100010001000, trace_flags 0x01 - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-01"), ("foo", "bar")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", + ), + call( + mock_carrier, + "tracestate", + TraceState( + [("sw", "1000100010001000-01"), ("foo", "bar")] + ).to_header(), + ), + ] + ) def test_inject_existing_tracestate_dict_no_stringvalue(self, mocker): """No tracestate creates new one""" self.mock_otel_context(mocker, True, trace_state=None) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) # OTel context mocked with span_id 0x1000100010001000, trace_flags 0x01 - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-01")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", + ), + call( + mock_carrier, + "tracestate", + TraceState([("sw", "1000100010001000-01")]).to_header(), + ), + ] + ) def test_inject_existing_tracestate_dict_no_sw(self, mocker): """sw added to start, foo=bar kept, xtrace_options_response removed""" - trace_state = TraceState([("xtrace_options_response", "abc123"), ("foo", "bar")]) + trace_state = TraceState( + [("xtrace_options_response", "abc123"), ("foo", "bar")] + ) self.mock_otel_context(mocker, True, trace_state=trace_state) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) # OTel context mocked with span_id 0x1000100010001000, trace_flags 0x01 - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-01"), ("foo", "bar")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", + ), + call( + mock_carrier, + "tracestate", + TraceState( + [("sw", "1000100010001000-01"), ("foo", "bar")] + ).to_header(), + ), + ] + ) def test_inject_existing_tracestate_dict_existing_sw(self, mocker): """sw updated and moved to start, foo=bar kept, xtrace_options_response removed""" - trace_state = TraceState([("xtrace_options_response", "abc123"), ("foo", "bar"), ("sw", "some-existing-value")]) + trace_state = TraceState( + [ + ("xtrace_options_response", "abc123"), + ("foo", "bar"), + ("sw", "some-existing-value"), + ] + ) self.mock_otel_context(mocker, True, trace_state=trace_state) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsPropagator().inject( mock_carrier, mock_context, mock_setter, ) # OTel context mocked with span_id 0x1000100010001000, trace_flags 0x01 - mock_set.assert_has_calls([ - call( - mock_carrier, - "traceparent", - "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", - ), - call( - mock_carrier, - "tracestate", - TraceState([("sw", "1000100010001000-01"), ("foo", "bar")]).to_header(), - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "traceparent", + "00-3a02f8a392478c3700000000deadbeef-1000100010001000-01", + ), + call( + mock_carrier, + "tracestate", + TraceState( + [("sw", "1000100010001000-01"), ("foo", "bar")] + ).to_header(), + ), + ] + ) class TestSolarWindsPropagatorNonDictCarriers: @@ -390,23 +389,17 @@ def mock_otel_context_with_tracestate(self, mocker, trace_state=None): """Mock OTel trace context with optional existing tracestate""" mock_get_span_context = mocker.Mock() mock_get_span_context.configure_mock( - **{ - "trace_id": 0x3A02F8A392478C3700000000DEADBEEF, - "span_id": 0x1000100010001000, - "trace_flags": 0x01, - "trace_state": trace_state, - } + trace_id=0x3A02F8A392478C3700000000DEADBEEF, + span_id=0x1000100010001000, + trace_flags=0x01, + trace_state=trace_state, ) mock_get_span_context.is_valid = True mock_get_current_span = mocker.Mock() mock_get_current_span.configure_mock( - **{ - "get_span_context.return_value": mock_get_span_context - } - ) - mock_trace = mocker.patch( - "solarwinds_apm.propagator.trace" + **{"get_span_context.return_value": mock_get_span_context} ) + mock_trace = mocker.patch("solarwinds_apm.propagator.trace") # Create a unique INVALID_SPAN_CONTEXT sentinel that won't match valid mocks mock_invalid_span_context = mocker.Mock() mock_trace.configure_mock( @@ -421,10 +414,12 @@ def test_inject_list_carrier_no_existing_tracestate(self, mocker): self.mock_otel_context_with_tracestate(mocker, None) mock_carrier = [] mock_context = mocker.Mock() + # Custom setter for list-based carrier (simulates KafkaContextSetter) class ListSetter(textmap.Setter): def set(self, carrier, key, value): carrier.append((key, value)) + mock_setter = ListSetter() SolarWindsPropagator().inject( @@ -433,7 +428,9 @@ def set(self, carrier, key, value): mock_setter, ) - tracestate_entries = [item for item in mock_carrier if item[0] == "tracestate"] + tracestate_entries = [ + item for item in mock_carrier if item[0] == "tracestate" + ] assert len(tracestate_entries) == 1 assert tracestate_entries[0][1] == "sw=1000100010001000-01" @@ -455,13 +452,17 @@ def set(self, carrier, key, value): mock_setter, ) - tracestate_entries = [item for item in mock_carrier if item[0] == "tracestate"] + tracestate_entries = [ + item for item in mock_carrier if item[0] == "tracestate" + ] assert len(tracestate_entries) == 1 assert tracestate_entries[0][1] == "sw=1000100010001000-01,foo=bar" def test_inject_list_carrier_updates_existing_sw(self, mocker): """Test injection updates existing sw in span_context.trace_state""" - existing_trace_state = TraceState([("sw", "old-value"), ("foo", "bar")]) + existing_trace_state = TraceState( + [("sw", "old-value"), ("foo", "bar")] + ) self.mock_otel_context_with_tracestate(mocker, existing_trace_state) mock_carrier = [] mock_context = mocker.Mock() @@ -477,7 +478,9 @@ def set(self, carrier, key, value): mock_setter, ) # Verify sw was updated and moved to front - tracestate_entries = [item for item in mock_carrier if item[0] == "tracestate"] + tracestate_entries = [ + item for item in mock_carrier if item[0] == "tracestate" + ] assert len(tracestate_entries) == 1 assert tracestate_entries[0][1] == "sw=1000100010001000-01,foo=bar" @@ -503,7 +506,9 @@ def set(self, carrier, key, value): assert True except AttributeError as e: if "'list' object has no attribute 'get'" in str(e): - raise AssertionError("Propagator incorrectly called carrier.get() on list carrier") + raise AssertionError( + "Propagator incorrectly called carrier.get() on list carrier" + ) from e raise def test_inject_custom_carrier_type(self, mocker): @@ -529,7 +534,10 @@ def set(self, carrier, key, value): mock_setter, ) assert "tracestate" in mock_carrier.headers - assert mock_carrier.headers["tracestate"] == "sw=1000100010001000-01,existing=value" + assert ( + mock_carrier.headers["tracestate"] + == "sw=1000100010001000-01,existing=value" + ) def test_inject_tuple_carrier(self, mocker): """Test that immutable carriers work with appropriate setter""" @@ -597,15 +605,19 @@ def set(self, carrier, key, value): propagator = SolarWindsPropagator() propagator.inject(carrier, context=ctxt, setter=tracking_setter) tracestate_writes = [ - call for call in tracking_setter.calls if call[0] == "tracestate" + call + for call in tracking_setter.calls + if call[0] == "tracestate" ] - assert ( - len(tracestate_writes) == 1 - ), f"Expected 1 tracestate write, got {len(tracestate_writes)}: {tracestate_writes}" - tracestate_entries = [item for item in carrier if item[0] == "tracestate"] - assert ( - len(tracestate_entries) == 1 - ), f"Expected 1 tracestate in carrier, got {len(tracestate_entries)}: {tracestate_entries}" + assert len(tracestate_writes) == 1, ( + f"Expected 1 tracestate write, got {len(tracestate_writes)}: {tracestate_writes}" + ) + tracestate_entries = [ + item for item in carrier if item[0] == "tracestate" + ] + assert len(tracestate_entries) == 1, ( + f"Expected 1 tracestate in carrier, got {len(tracestate_entries)}: {tracestate_entries}" + ) tracestate_value = tracestate_entries[0][1] assert "sw=" in tracestate_value assert "foo=bar" in tracestate_value diff --git a/tests/unit/test_response_propagator.py b/tests/unit/test_response_propagator.py index 95c3c580f..ef125e16d 100644 --- a/tests/unit/test_response_propagator.py +++ b/tests/unit/test_response_propagator.py @@ -6,10 +6,12 @@ from unittest.mock import call -from solarwinds_apm.response_propagator import SolarWindsTraceResponsePropagator +from solarwinds_apm.response_propagator import ( + SolarWindsTraceResponsePropagator, +) -class TestSwTraceResponsePropagator(): +class TestSwTraceResponsePropagator: def mock_otel_trace_and_sw(self, mocker, valid_span_context=True) -> None: """Shared mocks for OTel trace and some sw parts""" # Mock sw parts external to response propagator inject @@ -19,57 +21,41 @@ def mock_otel_trace_and_sw(self, mocker, valid_span_context=True) -> None: "solarwinds_apm.response_propagator.W3CTransformer" ) mock_w3ctransformer_cls.configure_mock( - **{ - "traceparent_from_context": mock_traceparent - } + traceparent_from_context=mock_traceparent ) mocker.patch( "solarwinds_apm.response_propagator.SolarWindsTraceResponsePropagator.recover_response_from_tracestate", - return_value="my_recovered_response" + return_value="my_recovered_response", ) # Mock OTel trace API and current span context mock_get_span_context = mocker.Mock() - mock_get_span_context.configure_mock( - **{ - "trace_state": "my_trace_state" - } - ) + mock_get_span_context.configure_mock(trace_state="my_trace_state") mock_get_current_span = mocker.Mock() if valid_span_context: mock_get_current_span.configure_mock( - **{ - "get_span_context.return_value": mock_get_span_context - } + **{"get_span_context.return_value": mock_get_span_context} ) else: mock_get_current_span.configure_mock( - **{ - "get_span_context.return_value": "INVALID" - } + **{"get_span_context.return_value": "INVALID"} ) - mock_trace = mocker.patch( - "solarwinds_apm.response_propagator.trace" - ) + mock_trace = mocker.patch("solarwinds_apm.response_propagator.trace") mock_trace.configure_mock( **{ "get_current_span.return_value": mock_get_current_span, - "INVALID_SPAN_CONTEXT": "INVALID" + "INVALID_SPAN_CONTEXT": "INVALID", } ) def test_inject_invalid_span_context(self, mocker): """The setter should not set anything for response headers""" self.mock_otel_trace_and_sw(mocker, False) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsTraceResponsePropagator().inject( mock_carrier, mock_context, @@ -81,15 +67,11 @@ def test_inject_invalid_span_context(self, mocker): def test_inject_valid_span_context_with_xtraceoptions(self, mocker): """The setter recovers x-trace-options response and sets in response headers""" self.mock_otel_trace_and_sw(mocker, True) - mock_carrier = dict() + mock_carrier = {} mock_context = mocker.Mock() mock_setter = mocker.Mock() mock_set = mocker.Mock() - mock_setter.configure_mock( - **{ - "set": mock_set - } - ) + mock_setter.configure_mock(set=mock_set) SolarWindsTraceResponsePropagator().inject( mock_carrier, mock_context, @@ -98,28 +80,28 @@ def test_inject_valid_span_context_with_xtraceoptions(self, mocker): SolarWindsTraceResponsePropagator.recover_response_from_tracestate.assert_called_once_with( "my_trace_state", ) - mock_set.assert_has_calls([ - call( - mock_carrier, - "x-trace", - "my_x_trace", - ), - call( - mock_carrier, - "x-trace-options-response", - "my_recovered_response", - ), - call( - mock_carrier, - "Access-Control-Expose-Headers", - "x-trace,x-trace-options-response" - ), - ]) + mock_set.assert_has_calls( + [ + call( + mock_carrier, + "x-trace", + "my_x_trace", + ), + call( + mock_carrier, + "x-trace-options-response", + "my_recovered_response", + ), + call( + mock_carrier, + "Access-Control-Expose-Headers", + "x-trace,x-trace-options-response", + ), + ] + ) def test_recover_response_from_tracestate(self, mocker): result = SolarWindsTraceResponsePropagator().recover_response_from_tracestate( - { - "xtrace_options_response": "bar####baz....qux####quux" - } + {"xtrace_options_response": "bar####baz....qux####quux"} ) assert result == "bar=baz,qux=quux" diff --git a/tests/unit/test_sampler/__init__.py b/tests/unit/test_sampler/__init__.py index 9167e547e..c3eb85d0c 100644 --- a/tests/unit/test_sampler/__init__.py +++ b/tests/unit/test_sampler/__init__.py @@ -2,4 +2,4 @@ # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/tests/unit/test_sampler/test_sampler.py b/tests/unit/test_sampler/test_sampler.py index c8832a76b..3338a3194 100644 --- a/tests/unit/test_sampler/test_sampler.py +++ b/tests/unit/test_sampler/test_sampler.py @@ -11,25 +11,25 @@ from solarwinds_apm.sampler import ParentBasedSwSampler -class TestParentBasedSwSampler(): +class TestParentBasedSwSampler: def test_init(self, mocker): mock_apm_config = mocker.Mock() mock_apm_config.get = mocker.Mock(return_value="foo") mock_apm_config.is_lambda = False sampler = ParentBasedSwSampler(mock_apm_config) - assert type(sampler._root) == HttpSampler - assert type(sampler._remote_parent_sampled) == HttpSampler - assert type(sampler._remote_parent_not_sampled) == HttpSampler - assert type(sampler._local_parent_sampled) == StaticSampler - assert type(sampler._local_parent_not_sampled) == StaticSampler + assert isinstance(sampler._root, HttpSampler) + assert isinstance(sampler._remote_parent_sampled, HttpSampler) + assert isinstance(sampler._remote_parent_not_sampled, HttpSampler) + assert isinstance(sampler._local_parent_sampled, StaticSampler) + assert isinstance(sampler._local_parent_not_sampled, StaticSampler) def test_init_is_lambda(self, mocker): mock_apm_config = mocker.Mock() mock_apm_config.get = mocker.Mock(return_value="foo") mock_apm_config.is_lambda = True sampler = ParentBasedSwSampler(mock_apm_config) - assert type(sampler._root) == JsonSampler - assert type(sampler._remote_parent_sampled) == JsonSampler - assert type(sampler._remote_parent_not_sampled) == JsonSampler - assert type(sampler._local_parent_sampled) == StaticSampler - assert type(sampler._local_parent_not_sampled) == StaticSampler \ No newline at end of file + assert isinstance(sampler._root, JsonSampler) + assert isinstance(sampler._remote_parent_sampled, JsonSampler) + assert isinstance(sampler._remote_parent_not_sampled, JsonSampler) + assert isinstance(sampler._local_parent_sampled, StaticSampler) + assert isinstance(sampler._local_parent_not_sampled, StaticSampler) diff --git a/tests/unit/test_semconv/__init__.py b/tests/unit/test_semconv/__init__.py index 9167e547e..c3eb85d0c 100644 --- a/tests/unit/test_semconv/__init__.py +++ b/tests/unit/test_semconv/__init__.py @@ -2,4 +2,4 @@ # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 # -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/tests/unit/test_semconv/test_trace.py b/tests/unit/test_semconv/test_trace.py index ef4e64a89..56e67aee2 100644 --- a/tests/unit/test_semconv/test_trace.py +++ b/tests/unit/test_semconv/test_trace.py @@ -6,13 +6,14 @@ from solarwinds_apm.semconv.trace import get_url_attrs -class Test_semconv_trace: + +class TestSemconvTrace: def test_get_url_attrs__new_path(self): attributes = { "url.scheme": "foo", "server.address": "bar", "server.port": "baz", - "url.path": "/qux" + "url.path": "/qux", } scheme, host, port, target = get_url_attrs(attributes) assert scheme == "foo" @@ -25,7 +26,7 @@ def test_get_url_attrs__new_query(self): "url.scheme": "foo", "server.address": "bar", "server.port": "baz", - "url.query": "/qux" + "url.query": "/qux", } scheme, host, port, target = get_url_attrs(attributes) assert scheme == "foo" @@ -38,7 +39,7 @@ def test_get_url_attrs__old(self): "http.scheme": "foo", "net.host.name": "bar", "net.host.port": "baz", - "http.target": "/qux" + "http.target": "/qux", } scheme, host, port, target = get_url_attrs(attributes) assert scheme == "foo" @@ -63,7 +64,7 @@ def test_get_url_attrs__prefer_new(self): "http.scheme": "OLD", "net.host.name": "OLD", "net.host.port": "OLD", - "http.target": "/OLD" + "http.target": "/OLD", } scheme, host, port, target = get_url_attrs(attributes) assert scheme == "foo" diff --git a/tests/unit/test_transaction_name_pool_api.py b/tests/unit/test_transaction_name_pool_api.py index e7c054749..8d4c58c82 100644 --- a/tests/unit/test_transaction_name_pool_api.py +++ b/tests/unit/test_transaction_name_pool_api.py @@ -5,10 +5,11 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. from solarwinds_apm.oboe import ( - get_transaction_name_pool, TransactionNamePool, + get_transaction_name_pool, ) + def test_get_transaction_name_pool(): local_pool = TransactionNamePool() global_pool = get_transaction_name_pool() diff --git a/tests/unit/test_uams.py b/tests/unit/test_uams.py index 7ed549422..fbc4e8683 100644 --- a/tests/unit/test_uams.py +++ b/tests/unit/test_uams.py @@ -8,18 +8,19 @@ import os import tempfile import uuid -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest from opentelemetry.semconv.resource import ResourceAttributes -from solarwinds_apm.uams import UamsResourceDetector, ATTR_UAMS_CLIENT_ID +from solarwinds_apm.uams import ATTR_UAMS_CLIENT_ID, UamsResourceDetector UAMS_FILE_ID = str(uuid.uuid4()) UAMS_API_ID = str(uuid.uuid4()) UAMS_FILE = os.path.join(tempfile.gettempdir(), "uamsclientid") + @pytest.fixture def setup_file(): os.makedirs(os.path.dirname(UAMS_FILE), exist_ok=True) @@ -28,8 +29,11 @@ def setup_file(): yield os.remove(UAMS_FILE) -@patch('requests.get') -def test_detects_id_from_file_when_file_present_and_api_running(mock_get, setup_file): + +@patch("requests.get") +def test_detects_id_from_file_when_file_present_and_api_running( + mock_get, setup_file +): mock_response = MagicMock() mock_response.json.return_value = { "uamsclient_id": UAMS_API_ID, @@ -45,7 +49,10 @@ def test_detects_id_from_file_when_file_present_and_api_running(mock_get, setup_ # Ensure the API was not called mock_get.assert_not_called() -def test_detects_id_from_file_when_file_present_and_api_not_running(setup_file): + +def test_detects_id_from_file_when_file_present_and_api_not_running( + setup_file, +): detector = UamsResourceDetector(UAMS_FILE) resource = detector.detect() assert resource.attributes == { @@ -53,8 +60,11 @@ def test_detects_id_from_file_when_file_present_and_api_not_running(setup_file): ResourceAttributes.HOST_ID: UAMS_FILE_ID, } -@patch('requests.get') -def test_detects_id_from_file_when_file_present_and_unrelated_running(mock_get, setup_file): + +@patch("requests.get") +def test_detects_id_from_file_when_file_present_and_unrelated_running( + mock_get, setup_file +): mock_response = MagicMock() mock_response.json.return_value = { "unrelated": "unrelated_value", @@ -70,7 +80,8 @@ def test_detects_id_from_file_when_file_present_and_unrelated_running(mock_get, # Ensure the API was not called mock_get.assert_not_called() -@patch('requests.get') + +@patch("requests.get") def test_detects_id_from_api_when_file_not_present_and_api_running(mock_get): mock_response = MagicMock() mock_response.json.return_value = { @@ -87,12 +98,14 @@ def test_detects_id_from_api_when_file_not_present_and_api_running(mock_get): # Ensure the API was called mock_get.assert_called_once() + def test_detects_nothing_when_file_not_present_and_api_not_running(): detector = UamsResourceDetector(UAMS_FILE) resource = detector.detect() assert resource.attributes == {} -@patch('requests.get') + +@patch("requests.get") def test_detects_nothing_when_file_not_present_and_unrelated_running(mock_get): mock_response = MagicMock() mock_response.json.return_value = { diff --git a/tests/unit/test_w3c_transformer.py b/tests/unit/test_w3c_transformer.py index 8701aaeae..62d5f99a9 100644 --- a/tests/unit/test_w3c_transformer.py +++ b/tests/unit/test_w3c_transformer.py @@ -5,7 +5,6 @@ # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. import pytest - from opentelemetry.trace.span import TraceState from solarwinds_apm.w3c_transformer import W3CTransformer @@ -17,22 +16,24 @@ def fixture_span_context(mocker): span_context_attrs = { "trace_id": 11112222333344445555666677778888, "span_id": 1111222233334444, - "trace_flags": 1, + "trace_flags": 1, } span_context.configure_mock(**span_context_attrs) return span_context -class TestW3CTransformer(): +class TestW3CTransformer: def test_span_from_int(self): - assert W3CTransformer.span_id_from_int(1111222233334444) \ - == "{:016x}".format(1111222233334444) + assert ( + W3CTransformer.span_id_from_int(1111222233334444) + == f"{1111222233334444:016x}" + ) def test_span_id_from_sw(self): assert W3CTransformer.span_id_from_sw("foo-bar") == "foo" def test_span_id_from_sw_invalid_type_returns_zero_fallback(self): - assert W3CTransformer.span_id_from_sw(None) == "{:016x}".format(0) + assert W3CTransformer.span_id_from_sw(None) == f"{0:016x}" @pytest.mark.parametrize( "trace_flags,expected", @@ -48,28 +49,30 @@ def test_trace_flags_from_int(self, trace_flags, expected): assert W3CTransformer.trace_flags_from_int(trace_flags) == expected def test_traceparent_from_context(self, span_context): - assert W3CTransformer.traceparent_from_context(span_context) \ - == "00-{:032x}-{:016x}-{:02x}".format( - span_context.trace_id, - span_context.span_id, - span_context.trace_flags - ) + assert ( + W3CTransformer.traceparent_from_context(span_context) + == f"00-{span_context.trace_id:032x}-{span_context.span_id:016x}-{span_context.trace_flags:02x}" + ) def test_sw_from_context(self, span_context): - assert W3CTransformer.sw_from_context(span_context) \ - == "{:016x}-{:02x}".format( - span_context.span_id, - span_context.trace_flags - ) + assert ( + W3CTransformer.sw_from_context(span_context) + == f"{span_context.span_id:016x}-{span_context.trace_flags:02x}" + ) def test_sw_from_span_and_decision(self): - assert W3CTransformer.sw_from_span_and_decision(1234, "01") \ - == "{:016x}-{}".format(1234, "01") + assert W3CTransformer.sw_from_span_and_decision( + 1234, "01" + ) == "{:016x}-{}".format(1234, "01") def test_remove_response_from_sw_key_present(self): - ts = TraceState([["bar", "456"],["xtrace_options_response", "123"]]) - assert W3CTransformer.remove_response_from_sw(ts) == TraceState([["bar", "456"]]) + ts = TraceState([["bar", "456"], ["xtrace_options_response", "123"]]) + assert W3CTransformer.remove_response_from_sw(ts) == TraceState( + [["bar", "456"]] + ) def test_remove_response_from_sw_key_absent(self): ts = TraceState([["bar", "456"]]) - assert W3CTransformer.remove_response_from_sw(ts) == TraceState([["bar", "456"]]) \ No newline at end of file + assert W3CTransformer.remove_response_from_sw(ts) == TraceState( + [["bar", "456"]] + ) diff --git a/tests/unit/test_xtraceoptions.py b/tests/unit/test_xtraceoptions.py index 8af1929b6..a67e7808a 100644 --- a/tests/unit/test_xtraceoptions.py +++ b/tests/unit/test_xtraceoptions.py @@ -7,7 +7,7 @@ from solarwinds_apm.traceoptions import XTraceOptions -class TestXTraceOptions(): +class TestXTraceOptions: def test_init_no_headers_defaults(self): xto = XTraceOptions() assert xto.ignored == [] @@ -101,11 +101,14 @@ def test_init_swkeys_key_value_strip(self): def test_init_swkeys_containing_semicolon_ignore_after(self): xto = XTraceOptions( - "sw-keys=check-id:check-1013,website-id;booking-demo" , + "sw-keys=check-id:check-1013,website-id;booking-demo", "bar", ) assert xto.ignored == ["booking-demo"] - assert xto.options_header == "sw-keys=check-id:check-1013,website-id;booking-demo" + assert ( + xto.options_header + == "sw-keys=check-id:check-1013,website-id;booking-demo" + ) assert xto.signature == "bar" assert xto.custom_kvs == {} assert xto.sw_keys == "check-id:check-1013,website-id" @@ -113,7 +116,9 @@ def test_init_swkeys_containing_semicolon_ignore_after(self): assert xto.timestamp == 0 assert xto.include_response - def test_init_custom_key_match_stored_in_options_header_and_custom_kvs(self): + def test_init_custom_key_match_stored_in_options_header_and_custom_kvs( + self, + ): xto = XTraceOptions("custom-awesome-key=foo", "bar") assert xto.ignored == [] assert xto.options_header == "custom-awesome-key=foo" @@ -124,11 +129,10 @@ def test_init_custom_key_match_stored_in_options_header_and_custom_kvs(self): assert xto.timestamp == 0 assert xto.include_response - def test_init_custom_key_match_stored_in_options_header_and_custom_kvs_strip(self): - xto = XTraceOptions( - "custom-awesome-key= foo ", - "bar" - ) + def test_init_custom_key_match_stored_in_options_header_and_custom_kvs_strip( + self, + ): + xto = XTraceOptions("custom-awesome-key= foo ", "bar") assert xto.ignored == [] assert xto.options_header == "custom-awesome-key= foo " assert xto.signature == "bar" @@ -155,9 +159,14 @@ def test_init_custom_key_match_equals_in_value_ok(self): "bar", ) assert xto.ignored == [] - assert xto.options_header == "custom-and=a-value=12345containing_equals=signs" + assert ( + xto.options_header + == "custom-and=a-value=12345containing_equals=signs" + ) assert xto.signature == "bar" - assert xto.custom_kvs == {"custom-and": "a-value=12345containing_equals=signs"} + assert xto.custom_kvs == { + "custom-and": "a-value=12345containing_equals=signs" + } assert xto.sw_keys == "" assert xto.trigger_trace == 0 assert xto.timestamp == 0 @@ -169,7 +178,10 @@ def test_init_custom_key_spaces_in_key_not_allowed(self): "bar", ) assert xto.ignored == ["custom- key", "custom-key 7"] - assert xto.options_header == "custom- key=this_is_bad;custom-key 7=this_is_bad_too" + assert ( + xto.options_header + == "custom- key=this_is_bad;custom-key 7=this_is_bad_too" + ) assert xto.signature == "bar" assert xto.custom_kvs == {} assert xto.sw_keys == "" @@ -216,7 +228,10 @@ def test_init_xtraceoptions_documented_example_1(self): "bar", ) assert xto.ignored == [] - assert xto.options_header == "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo" + assert ( + xto.options_header + == "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo" + ) assert xto.signature == "bar" assert xto.custom_kvs == {} assert xto.sw_keys == "check-id:check-1013,website-id:booking-demo" @@ -244,7 +259,10 @@ def test_init_xtraceoptions_documented_example_3(self): "bar", ) assert xto.ignored == [] - assert xto.options_header == "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;ts=1564432370" + assert ( + xto.options_header + == "trigger-trace;sw-keys=check-id:check-1013,website-id:booking-demo;ts=1564432370" + ) assert xto.signature == "bar" assert xto.custom_kvs == {} assert xto.sw_keys == "check-id:check-1013,website-id:booking-demo" @@ -258,7 +276,10 @@ def test_init_all_options_strip(self): "bar", ) assert xto.ignored == ["foo"] - assert xto.options_header == " trigger-trace ; custom-something=value; custom-OtherThing = other val ; sw-keys = 029734wr70:9wqj21,0d9j1 ; ts = 12345 ; foo = bar " + assert ( + xto.options_header + == " trigger-trace ; custom-something=value; custom-OtherThing = other val ; sw-keys = 029734wr70:9wqj21,0d9j1 ; ts = 12345 ; foo = bar " + ) assert xto.signature == "bar" assert xto.custom_kvs == { "custom-something": "value", @@ -275,7 +296,10 @@ def test_init_all_options_handle_sequential_semis(self): "bar", ) assert xto.ignored == ["foo"] - assert xto.options_header == ";foo=bar;;;custom-something=value_thing;;sw-keys=02973r70:1b2a3;;;;custom-key=val;ts=12345;;;;;;;trigger-trace;;;" + assert ( + xto.options_header + == ";foo=bar;;;custom-something=value_thing;;sw-keys=02973r70:1b2a3;;;;custom-key=val;ts=12345;;;;;;;trigger-trace;;;" + ) assert xto.signature == "bar" assert xto.custom_kvs == { "custom-something": "value_thing", @@ -292,7 +316,10 @@ def test_init_keep_first_repeated_key_value(self): "bar", ) assert xto.ignored == [] - assert xto.options_header == "ts=123;custom-something=keep_this_0;sw-keys=keep_this;sw-keys=029734wrqj21,0d9;custom-something=otherval;ts=456" + assert ( + xto.options_header + == "ts=123;custom-something=keep_this_0;sw-keys=keep_this;sw-keys=029734wrqj21,0d9;custom-something=otherval;ts=456" + ) assert xto.signature == "bar" assert xto.custom_kvs == { "custom-something": "keep_this_0", @@ -308,7 +335,10 @@ def test_init_keep_values_containing_equals_char(self): "bar", ) assert xto.ignored == [] - assert xto.options_header == "trigger-trace;custom-something=value_thing=4;custom-OtherThing=other val;sw-keys=g049sj345=0spd" + assert ( + xto.options_header + == "trigger-trace;custom-something=value_thing=4;custom-OtherThing=other val;sw-keys=g049sj345=0spd" + ) assert xto.signature == "bar" assert xto.custom_kvs == { "custom-something": "value_thing=4", @@ -325,7 +355,10 @@ def test_init_single_quotes_are_ok(self): "bar", ) assert xto.ignored == ["bar'"] - assert xto.options_header == "trigger-trace;custom-foo='bar;bar';custom-bar=foo" + assert ( + xto.options_header + == "trigger-trace;custom-foo='bar;bar';custom-bar=foo" + ) assert xto.signature == "bar" assert xto.custom_kvs == { "custom-foo": "'bar", @@ -342,7 +375,10 @@ def test_init_multiple_missing_values_and_semis(self): "bar", ) assert xto.ignored == ["1", "2", "3", "4", "5"] - assert xto.options_header == ";trigger-trace;custom-something=value_thing;sw-keys=02973r70:9wqj21,0d9j1;1;2;3;4;5;=custom-key=val?;=" + assert ( + xto.options_header + == ";trigger-trace;custom-something=value_thing;sw-keys=02973r70:9wqj21,0d9j1;1;2;3;4;5;=custom-key=val?;=" + ) assert xto.signature == "bar" assert xto.custom_kvs == { "custom-something": "value_thing", From 063bb79540fa323fc2b32db61fe429df5fc4e125 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 19 Aug 2026 12:31:34 -0700 Subject: [PATCH 3/4] Ignore ruff UP038 because deprecated --- pyproject.toml | 1 + tests/unit/test_oboe/test_token_bucket.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 52ac27c5b..ee443a3b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ ignore = [ "E203", "B008", # for Resource.create() test compatibility "UP006", # Use `set` instead of `typing.Set` type annotation, for compatibility with upstream SDK state + "UP038", # deprecated by Astral for performance ] [tool.ruff.lint.isort] diff --git a/tests/unit/test_oboe/test_token_bucket.py b/tests/unit/test_oboe/test_token_bucket.py index b6cba9d77..a868bafa7 100644 --- a/tests/unit/test_oboe/test_token_bucket.py +++ b/tests/unit/test_oboe/test_token_bucket.py @@ -133,7 +133,7 @@ def read_properties(): assert len(errors) == 0 assert all(c == 50 for c in results["capacity"]) assert all(r == 5 for r in results["rate"]) - assert all(isinstance(t, (int | float)) for t in results["tokens"]) + assert all(isinstance(t, (int, float)) for t in results["tokens"]) def test_concurrent_update_and_consume(): From 10000da1b74fa4dc95426cda088f0a437281d494 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 19 Aug 2026 12:47:43 -0700 Subject: [PATCH 4/4] Rm unnecessary disable=unused-import --- tests/unit/test_apm_config/test_apm_config.py | 2 -- tests/unit/test_apm_config/test_apm_config_agent_enabled.py | 4 ---- tests/unit/test_apm_config/test_apm_config_cnf_file.py | 6 ------ tests/unit/test_apm_config/test_apm_config_service_name.py | 2 -- 4 files changed, 14 deletions(-) diff --git a/tests/unit/test_apm_config/test_apm_config.py b/tests/unit/test_apm_config/test_apm_config.py index e58aafcd9..2d2472133 100644 --- a/tests/unit/test_apm_config/test_apm_config.py +++ b/tests/unit/test_apm_config/test_apm_config.py @@ -15,8 +15,6 @@ from solarwinds_apm import apm_config from solarwinds_apm.oboe.configuration import Configuration, TransactionSetting -# pylint: disable=unused-import - @pytest.fixture def setup_caplog(): diff --git a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py index 1fa18dfc5..d38243b01 100644 --- a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py +++ b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py @@ -11,10 +11,6 @@ from solarwinds_apm import apm_config -# pylint: disable=unused-import - -# pylint: disable=unused-import - @pytest.fixture def setup_caplog(): diff --git a/tests/unit/test_apm_config/test_apm_config_cnf_file.py b/tests/unit/test_apm_config/test_apm_config_cnf_file.py index 70fdeb1ec..88bd9df9f 100644 --- a/tests/unit/test_apm_config/test_apm_config_cnf_file.py +++ b/tests/unit/test_apm_config/test_apm_config_cnf_file.py @@ -9,12 +9,6 @@ from solarwinds_apm import apm_config -# pylint: disable=unused-import - -# pylint: disable=unused-import - -# pylint: disable=unused-import - class TestSolarWindsApmConfigCnfFile: # pylint:disable=unused-argument diff --git a/tests/unit/test_apm_config/test_apm_config_service_name.py b/tests/unit/test_apm_config/test_apm_config_service_name.py index b99b6eceb..0dc1616f9 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_name.py +++ b/tests/unit/test_apm_config/test_apm_config_service_name.py @@ -10,8 +10,6 @@ from solarwinds_apm import apm_config -# pylint: disable=unused-import - class TestSolarWindsApmConfigServiceName: def test__calculate_service_name_is_lambda(self, mocker):