diff --git a/src/instana/agent/base.py b/src/instana/agent/base.py index 3c6f63eaf..08e68f062 100644 --- a/src/instana/agent/base.py +++ b/src/instana/agent/base.py @@ -4,13 +4,17 @@ """ Base class for all the agent flavors """ + import logging + import requests -from ..log import logger + +from instana.log import logger class BaseAgent(object): - """ Base class for all agent flavors """ + """Base class for all agent flavors""" + client = None options = None @@ -18,13 +22,14 @@ def __init__(self): self.client = requests.Session() def update_log_level(self): - """ Uses the value in to update the global logger """ - if self.options is None or self.options.log_level not in [logging.DEBUG, - logging.INFO, - logging.WARN, - logging.ERROR]: + """Uses the value in to update the global logger""" + if self.options is None or self.options.log_level not in [ + logging.DEBUG, + logging.INFO, + logging.WARN, + logging.ERROR, + ]: logger.warning("BaseAgent.update_log_level: Unknown log level set") return logger.setLevel(self.options.log_level) - diff --git a/tests/agent/test_google_cloud_run.py b/tests/agent/test_google_cloud_run.py new file mode 100644 index 000000000..3ef932d64 --- /dev/null +++ b/tests/agent/test_google_cloud_run.py @@ -0,0 +1,151 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2021 + +import logging +import os +from typing import Generator + +import pytest + +from instana.agent.google_cloud_run import GCRAgent +from instana.options import GCROptions +from instana.recorder import StanRecorder +from instana.singletons import get_agent, get_tracer, set_agent, set_tracer +from instana.tracer import InstanaTracer, InstanaTracerProvider + + +class TestGCR: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.agent = None + self.span_recorder = None + self.tracer = None + + self.original_agent = get_agent() + self.original_tracer = get_tracer() + + os.environ["K_SERVICE"] = "service" + os.environ["K_CONFIGURATION"] = "configuration" + os.environ["K_REVISION"] = "revision" + os.environ["PORT"] = "port" + os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" + os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" + yield + if "K_SERVICE" in os.environ: + os.environ.pop("K_SERVICE") + if "K_CONFIGURATION" in os.environ: + os.environ.pop("K_CONFIGURATION") + if "K_REVISION" in os.environ: + os.environ.pop("K_REVISION") + if "PORT" in os.environ: + os.environ.pop("PORT") + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_ENDPOINT_PROXY" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_PROXY") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + if "INSTANA_LOG_LEVEL" in os.environ: + os.environ.pop("INSTANA_LOG_LEVEL") + if "INSTANA_SECRETS" in os.environ: + os.environ.pop("INSTANA_SECRETS") + if "INSTANA_DEBUG" in os.environ: + os.environ.pop("INSTANA_DEBUG") + if "INSTANA_TAGS" in os.environ: + os.environ.pop("INSTANA_TAGS") + + set_agent(self.original_agent) + set_tracer(self.original_tracer) + + def create_agent_and_setup_tracer( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.agent = GCRAgent( + service="service", + configuration="configuration", + revision="revision", + ) + self.span_processor = StanRecorder(self.agent) + self.tracer = InstanaTracer( + tracer_provider.sampler, + self.span_processor, + tracer_provider._exporter, + tracer_provider._propagators, + ) + set_agent(self.agent) + set_tracer(self.tracer) + + def test_has_options(self, tracer_provider: InstanaTracerProvider) -> None: + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, GCROptions) + + def test_invalid_options(self): + # None of the required env vars are available... + if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: + os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") + if "INSTANA_ENDPOINT_URL" in os.environ: + os.environ.pop("INSTANA_ENDPOINT_URL") + if "INSTANA_AGENT_KEY" in os.environ: + os.environ.pop("INSTANA_AGENT_KEY") + + agent = GCRAgent( + service="service", configuration="configuration", revision="revision" + ) + assert not agent.can_send() + assert not agent.collector + + def test_default_secrets(self, tracer_provider: InstanaTracerProvider) -> None: + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + assert not self.agent.options.secrets + assert hasattr(self.agent.options, "secrets_matcher") + assert self.agent.options.secrets_matcher == "contains-ignore-case" + assert hasattr(self.agent.options, "secrets_list") + assert self.agent.options.secrets_list == ["key", "pass", "secret"] + + def test_custom_secrets(self, tracer_provider: InstanaTracerProvider) -> None: + os.environ["INSTANA_SECRETS"] = "equals:love,war,games" + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + + assert hasattr(self.agent.options, "secrets_matcher") + assert self.agent.options.secrets_matcher == "equals" + assert hasattr(self.agent.options, "secrets_list") + assert self.agent.options.secrets_list == ["love", "war", "games"] + + def test_has_extra_http_headers( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") + + def test_agent_extra_http_headers( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["INSTANA_EXTRA_HTTP_HEADERS"] = ( + "X-Test-Header;X-Another-Header;X-And-Another-Header" + ) + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + assert self.agent.options.extra_http_headers + should_headers = ["x-test-header", "x-another-header", "x-and-another-header"] + assert should_headers == self.agent.options.extra_http_headers + + def test_agent_default_log_level( + self, tracer_provider: InstanaTracerProvider + ) -> None: + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + assert self.agent.options.log_level == logging.WARNING + + def test_agent_custom_log_level( + self, tracer_provider: InstanaTracerProvider + ) -> None: + os.environ["INSTANA_LOG_LEVEL"] = "eRror" + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + assert self.agent.options.log_level == logging.ERROR + + def test_custom_proxy(self, tracer_provider: InstanaTracerProvider) -> None: + os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" + self.create_agent_and_setup_tracer(tracer_provider=tracer_provider) + assert self.agent.options.endpoint_proxy == {"https": "http://myproxy.123"} diff --git a/tests/agent/test_host.py b/tests/agent/test_host.py index ec130aa2d..cff710620 100644 --- a/tests/agent/test_host.py +++ b/tests/agent/test_host.py @@ -1,310 +1,602 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + import datetime import json import logging import os - -from unittest.mock import Mock, patch +from typing import Generator +from unittest.mock import Mock import pytest import requests +from mock import MagicMock, patch + from instana.agent.host import AnnounceData, HostAgent from instana.collector.host import HostCollector -from instana.fsm import TheMachine +from instana.fsm import Discovery, TheMachine from instana.options import StandardOptions from instana.recorder import StanRecorder +from instana.singletons import get_agent from instana.span.span import InstanaSpan from instana.span_context import SpanContext -from pytest import LogCaptureFixture -def test_init(): - with patch( - "instana.agent.base.BaseAgent.update_log_level" - ) as mock_update, patch.object(os, "getpid", return_value=12345): - agent = HostAgent() - assert not agent.announce_data - assert not agent.last_seen - assert not agent.last_fork_check - assert agent._boot_pid == 12345 +class TestHostAgent: + @pytest.fixture(autouse=True) + def _resource( + self, + caplog: pytest.LogCaptureFixture, + ) -> Generator[None, None, None]: + self.agent = get_agent() + self.span_recorder = None + self.tracer = None + yield + caplog.clear() + variable_names = ( + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_ENDPOINT_PROXY", + "INSTANA_AGENT_KEY", + "INSTANA_LOG_LEVEL", + "INSTANA_SERVICE_NAME", + "INSTANA_SECRETS", + "INSTANA_TAGS", + ) - mock_update.assert_called_once() + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + def test_secrets(self) -> None: + assert hasattr(self.agent.options, "secrets_matcher") + assert self.agent.options.secrets_matcher == "contains-ignore-case" + assert hasattr(self.agent.options, "secrets_list") + assert self.agent.options.secrets_list == ["key", "pass", "secret"] + + def test_options_have_extra_http_headers(self) -> None: + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") + + def test_has_options(self) -> None: + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, StandardOptions) + + def test_agent_default_log_level(self) -> None: + assert self.agent.options.log_level == logging.WARNING + + def test_agent_instana_debug(self) -> None: + os.environ["INSTANA_DEBUG"] = "asdf" + self.agent.options = StandardOptions() + assert self.agent.options.log_level == logging.DEBUG + + def test_agent_instana_service_name(self) -> None: + os.environ["INSTANA_SERVICE_NAME"] = "greycake" + self.agent.options = StandardOptions() + assert self.agent.options.service_name == "greycake" + + @patch.object(requests.Session, "put") + def test_announce_is_successful( + self, + mock_requests_session_put: MagicMock, + ) -> None: + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = ( + "{" f' "pid": {test_pid}, ' f' "agentUuid": "{test_agent_uuid}"' "}" + ) - assert isinstance(agent.options, StandardOptions) - assert isinstance(agent.collector, HostCollector) - assert isinstance(agent.machine, TheMachine) + # This mocks the call to self.agent.client.put + mock_requests_session_put.return_value = mock_response + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) -def test_start(): - with patch("instana.collector.host.HostCollector.start") as mock_start: - agent = HostAgent() - agent.start() - mock_start.assert_called_once() + assert "pid" in payload + assert test_pid == payload["pid"] + assert "agentUuid" in payload + assert test_agent_uuid == payload["agentUuid"] -def test_handle_fork(): - with patch.object(HostAgent, "reset") as mock_reset: - agent = HostAgent() - agent.handle_fork() - mock_reset.assert_called_once() + @patch.object(requests.Session, "put") + def test_announce_fails_with_non_200( + self, + mock_requests_session_put: MagicMock, + caplog: pytest.LogCaptureFixture, + ) -> None: + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + mock_response = MagicMock() + mock_response.status_code = 404 + mock_response.content = "" + mock_requests_session_put.return_value = mock_response -def test_reset(): - with patch("instana.collector.host.HostCollector.shutdown") as mock_shutdown, patch( - "instana.fsm.TheMachine.reset" - ) as mock_reset: - agent = HostAgent() - agent.reset() - - assert not agent.last_seen - assert not agent.announce_data - - mock_shutdown.assert_called_once_with(report_final=False) - mock_reset.assert_called_once() - - -def test_is_timed_out(): - agent = HostAgent() - assert not agent.is_timed_out() - - agent.last_seen = datetime.datetime.now() - datetime.timedelta(minutes=5) - agent.can_send = True - assert agent.is_timed_out() - - -@pytest.mark.original -def test_can_send(): - agent = HostAgent() - agent._boot_pid = 12345 - with patch.object(os, "getpid", return_value=12344), patch( - "instana.agent.host.HostAgent.handle_fork" - ) as mock_handle, patch.dict("os.environ", {}, clear=True): - agent.can_send() - assert agent._boot_pid == 12344 - mock_handle.assert_called_once() - - with patch.object(agent.machine.fsm, "current", "wait4init"): - assert agent.can_send() is True - - -@pytest.mark.original -def test_can_send_default(): - agent = HostAgent() - with patch.dict("os.environ", {}, clear=True): - assert not agent.can_send() - - -def test_set_from(): - agent = HostAgent() - sample_res_data = { - "secrets": {"matcher": "value-1", "list": ["value-2"]}, - "extraHeaders": ["value-3"], - "agentUuid": "value-4", - "pid": 1234, - } - agent.options.extra_http_headers = None - - agent.set_from(sample_res_data) - assert agent.options.secrets_matcher == "value-1" - assert agent.options.secrets_list == ["value-2"] - assert agent.options.extra_http_headers == ["value-3"] - - agent.options.extra_http_headers = ["value"] - agent.set_from(sample_res_data) - assert "value" in agent.options.extra_http_headers - - assert agent.announce_data.agentUuid == "value-4" - assert agent.announce_data.pid == 1234 - - -@pytest.mark.original -def test_get_from_structure(): - agent = HostAgent() - agent.announce_data = AnnounceData(pid=1234, agentUuid="value") - assert agent.get_from_structure() == {"e": 1234, "h": "value"} - -@pytest.mark.original -def test_is_agent_listening( - caplog: LogCaptureFixture, -): - agent = HostAgent() - mock_response = Mock() - mock_response.status_code = 200 - with patch.object(requests.Session, "get", return_value=mock_response): - assert agent.is_agent_listening("sample", 1234) - - mock_response.status_code = 404 - with patch.object(requests.Session, "get", return_value=mock_response, clear=True): - assert not agent.is_agent_listening("sample", 1234) - - host = "localhost" - port = 123 - with patch.object(requests.Session, "get", side_effect=Exception()): + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) caplog.set_level(logging.DEBUG, logger="instana") - agent.is_agent_listening(host, port) - assert f"Instana Host Agent not found on {host}:{port}" in caplog.messages - - -def test_announce( - caplog: LogCaptureFixture, -): - agent = HostAgent() - mock_response = Mock() - mock_response.status_code = 200 - mock_response.content = json.dumps( - {"get": "value", "pid": "value", "agentUuid": "value"} - ) - response = json.loads(mock_response.content) - with patch.object(requests.Session, "put", return_value=mock_response): - assert agent.announce("sample-data") == response - - mock_response.content = mock_response.content.encode("UTF-8") - with patch.object(requests.Session, "put", return_value=mock_response): - assert agent.announce("sample-data") == response - - mock_response.content = json.dumps( - {"get": "value", "pid": "value", "agentUuid": "value"} - ) - - with patch.object(requests.Session, "put", side_effect=Exception()): + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response status code" in caplog.messages[0] + assert "is NOT 200" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_non_json( + self, + mock_requests_session_put: MagicMock, + caplog: pytest.LogCaptureFixture, + ) -> None: + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "" + mock_requests_session_put.return_value = mock_response + + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) caplog.set_level(logging.DEBUG, logger="instana") - assert not agent.announce("sample-data") - assert f"announce: connection error ({type(Exception())})" in caplog.messages - - mock_response.content = json.dumps("key") - with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response is not JSON" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_empty_list_json( + self, + mock_requests_session_put: MagicMock, + caplog: pytest.LogCaptureFixture, + ) -> None: + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "[]" + mock_requests_session_put.return_value = mock_response + + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) caplog.set_level(logging.DEBUG, logger="instana") - assert not agent.announce("sample-data") - assert "announce: response payload has no fields: (key)" in caplog.messages - - mock_response.content = json.dumps({"key": "value"}) - with patch.object(requests.Session, "put", return_value=mock_response, clear=True): + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "payload has no fields" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_missing_pid( + self, + mock_requests_session_put: MagicMock, + caplog: pytest.LogCaptureFixture, + ) -> None: caplog.set_level(logging.DEBUG, logger="instana") - assert not agent.announce("sample-data") - assert ( - "announce: response payload has no pid: ({'key': 'value'})" - in caplog.messages + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "{" f' "agentUuid": "{test_agent_uuid}"' "}" + mock_requests_session_put.return_value = mock_response + + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no pid" in caplog.messages[0] + + @patch.object(requests.Session, "put") + def test_announce_fails_with_missing_uuid( + self, + mock_requests_session_put: MagicMock, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + test_pid = 4242 + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.content = "{" f' "pid": {test_pid} ' "}" + mock_requests_session_put.return_value = mock_response + + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + caplog.clear() + payload = self.agent.announce(d) + assert payload is None + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no agentUuid" in caplog.messages[0] + + @pytest.mark.original + @patch.object(requests.Session, "get") + def test_agent_connection_attempt( + self, + mock_requests_session_get: MagicMock, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + mock_response = MagicMock() + mock_response.status_code = 200 + mock_requests_session_get.return_value = mock_response + + host = self.agent.options.agent_host + port = self.agent.options.agent_port + msg = f"Instana host agent found on {host}:{port}" + + result = self.agent.is_agent_listening(host, port) + + assert result + assert msg in caplog.messages[0] + + @pytest.mark.original + @patch.object(requests.Session, "get") + def test_agent_connection_attempt_fails_with_404( + self, + mock_requests_session_get: MagicMock, + caplog: pytest.LogCaptureFixture, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + mock_response = MagicMock() + mock_response.status_code = 404 + mock_requests_session_get.return_value = mock_response + + host = self.agent.options.agent_host + port = self.agent.options.agent_port + msg = ( + "The attempt to connect to the Instana host agent on " + f"{host}:{port} has failed with an unexpected status code. " + f"Expected HTTP 200 but received: {mock_response.status_code}" ) - mock_response.content = json.dumps({"pid": "value"}) - with patch.object(requests.Session, "put", return_value=mock_response, clear=True): - caplog.set_level(logging.DEBUG, logger="instana") - assert not agent.announce("sample-data") - assert ( - "announce: response payload has no agentUuid: ({'pid': 'value'})" - in caplog.messages + caplog.clear() + result = self.agent.is_agent_listening(host, port) + + assert not result + assert msg in caplog.messages[0] + + def test_init(self) -> None: + with patch( + "instana.agent.base.BaseAgent.update_log_level" + ) as mock_update, patch.object(os, "getpid", return_value=12345): + agent = HostAgent() + assert not agent.announce_data + assert not agent.last_seen + assert not agent.last_fork_check + assert agent._boot_pid == 12345 + + mock_update.assert_called_once() + + assert isinstance(agent.options, StandardOptions) + assert isinstance(agent.collector, HostCollector) + assert isinstance(agent.machine, TheMachine) + + def test_start( + self, + ) -> None: + with patch("instana.collector.host.HostCollector.start") as mock_start: + agent = HostAgent() + agent.start() + mock_start.assert_called_once() + + def test_handle_fork( + self, + ) -> None: + with patch.object(HostAgent, "reset") as mock_reset: + agent = HostAgent() + agent.handle_fork() + mock_reset.assert_called_once() + + def test_reset( + self, + ) -> None: + with patch( + "instana.collector.host.HostCollector.shutdown" + ) as mock_shutdown, patch("instana.fsm.TheMachine.reset") as mock_reset: + agent = HostAgent() + agent.reset() + + assert not agent.last_seen + assert not agent.announce_data + + mock_shutdown.assert_called_once_with(report_final=False) + mock_reset.assert_called_once() + + def test_is_timed_out( + self, + ) -> None: + agent = HostAgent() + assert not agent.is_timed_out() + + agent.last_seen = datetime.datetime.now() - datetime.timedelta(minutes=5) + agent.can_send = True + assert agent.is_timed_out() + + def test_can_send_test_env( + self, + ) -> None: + agent = HostAgent() + with patch.dict("os.environ", {"INSTANA_TEST": "sample-data"}): + if "INSTANA_TEST" in os.environ: + assert agent.can_send() + + @pytest.mark.original + def test_can_send( + self, + ) -> None: + agent = HostAgent() + agent._boot_pid = 12345 + with patch.object(os, "getpid", return_value=12344), patch( + "instana.agent.host.HostAgent.handle_fork" + ) as mock_handle, patch.dict("os.environ", {}, clear=True): + agent.can_send() + assert agent._boot_pid == 12344 + mock_handle.assert_called_once() + + with patch.object(agent.machine.fsm, "current", "wait4init"): + assert agent.can_send() is True + + @pytest.mark.original + def test_can_send_default( + self, + ) -> None: + agent = HostAgent() + with patch.dict("os.environ", {}, clear=True): + assert not agent.can_send() + + def test_set_from( + self, + ) -> None: + agent = HostAgent() + sample_res_data = { + "secrets": {"matcher": "value-1", "list": ["value-2"]}, + "extraHeaders": ["value-3"], + "agentUuid": "value-4", + "pid": 1234, + } + agent.options.extra_http_headers = None + + agent.set_from(sample_res_data) + assert agent.options.secrets_matcher == "value-1" + assert agent.options.secrets_list == ["value-2"] + assert agent.options.extra_http_headers == ["value-3"] + + agent.options.extra_http_headers = ["value"] + agent.set_from(sample_res_data) + assert "value" in agent.options.extra_http_headers + + assert agent.announce_data.agentUuid == "value-4" + assert agent.announce_data.pid == 1234 + + @pytest.mark.original + def test_get_from_structure( + self, + ) -> None: + agent = HostAgent() + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + assert agent.get_from_structure() == {"e": 1234, "h": "value"} + + @pytest.mark.original + def test_is_agent_listening( + self, + caplog, + ) -> None: + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + with patch.object(requests.Session, "get", return_value=mock_response): + assert agent.is_agent_listening("sample", 1234) + + mock_response.status_code = 404 + with patch.object( + requests.Session, "get", return_value=mock_response, clear=True + ): + assert not agent.is_agent_listening("sample", 1234) + + host = "localhost" + port = 123 + with patch.object(requests.Session, "get", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_listening(host, port) + assert f"Instana Host Agent not found on {host}:{port}" in caplog.messages + + @pytest.mark.original + def test_announce( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} + ) + response = json.loads(mock_response.content) + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = mock_response.content.encode("UTF-8") + with patch.object(requests.Session, "put", return_value=mock_response): + assert agent.announce("sample-data") == response + + mock_response.content = json.dumps( + {"get": "value", "pid": "value", "agentUuid": "value"} ) - mock_response.status_code = 404 - with patch.object(requests.Session, "put", return_value=mock_response, clear=True): - assert not agent.announce("sample-data") - assert "announce: response status code (404) is NOT 200" in caplog.messages - - -def test_log_message_to_host_agent( - caplog: LogCaptureFixture, -): - agent = HostAgent() - mock_response = Mock() - mock_response.status_code = 200 - mock_response.return_value = "sample" - mock_datetime = datetime.datetime(2022, 1, 1, 12, 0, 0) - with patch.object(requests.Session, "post", return_value=mock_response), patch( - "instana.agent.host.datetime" - ) as mock_date: - mock_date.now.return_value = mock_datetime - mock_date.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) - agent.log_message_to_host_agent("sample") - assert agent.last_seen == mock_datetime - - with patch.object(requests.Session, "post", side_effect=Exception()): + with patch.object(requests.Session, "put", side_effect=Exception()): caplog.set_level(logging.DEBUG, logger="instana") - agent.log_message_to_host_agent("sample") + assert not agent.announce("sample-data") assert ( - f"agent logging: connection error ({type(Exception())})" - in caplog.messages + f"announce: connection error ({type(Exception())})" in caplog.messages ) + mock_response.content = json.dumps("key") + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert "announce: response payload has no fields: (key)" in caplog.messages -def test_is_agent_ready(caplog: LogCaptureFixture): - agent = HostAgent() - mock_response = Mock() - mock_response.status_code = 200 - mock_response.return_value = {"key": "value"} - agent.AGENT_DATA_PATH = "sample_path" - agent.announce_data = AnnounceData(pid=1234, agentUuid="sample") - with patch.object(requests.Session, "head", return_value=mock_response), patch( - "instana.agent.host.HostAgent._HostAgent__data_url", return_value="localhost" - ): - assert agent.is_agent_ready() - with patch.object(requests.Session, "head", side_effect=Exception()): + mock_response.content = json.dumps({"key": "value"}) + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): caplog.set_level(logging.DEBUG, logger="instana") - agent.is_agent_ready() + assert not agent.announce("sample-data") assert ( - f"is_agent_ready: connection error ({type(Exception())})" + "announce: response payload has no pid: ({'key': 'value'})" in caplog.messages ) + mock_response.content = json.dumps({"pid": "value"}) + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + caplog.set_level(logging.DEBUG, logger="instana") + assert not agent.announce("sample-data") + assert ( + "announce: response payload has no agentUuid: ({'pid': 'value'})" + in caplog.messages + ) -def test_report_data_payload( - span_context: SpanContext, - span_processor: StanRecorder, -): - agent = HostAgent() - span_name = "test-span" - span_1 = InstanaSpan(span_name, span_context, span_processor) - span_2 = InstanaSpan(span_name, span_context, span_processor) - payload = { - "spans": [span_1, span_2], - "profiles": ["profile-1", "profile-2"], - "metrics": { - "plugins": [ - {"data": "sample data"}, - ] - }, - } - sample_response = {"key": "value"} - mock_response = Mock() - mock_response.status_code = 200 - mock_response.content = sample_response - with patch.object(requests.Session, "post", return_value=mock_response), patch( - "instana.agent.host.HostAgent._HostAgent__traces_url", return_value="localhost" - ), patch( - "instana.agent.host.HostAgent._HostAgent__profiles_url", - return_value="localhost", - ), patch( - "instana.agent.host.HostAgent._HostAgent__data_url", return_value="localhost" - ): - test_response = agent.report_data_payload(payload) - assert isinstance(agent.last_seen, datetime.datetime) - assert test_response.content == sample_response - - -def test_diagnostics(caplog: LogCaptureFixture): - caplog.set_level(logging.WARNING, logger="instana") - - agent = HostAgent() - agent.diagnostics() - assert "====> Instana Python Language Agent Diagnostics <====" in caplog.messages - assert "----> Agent <----" in caplog.messages - assert f"is_agent_ready: {agent.is_agent_ready()}" in caplog.messages - assert f"is_timed_out: {agent.is_timed_out()}" in caplog.messages - assert "last_seen: None" in caplog.messages - - sample_date = datetime.datetime(2022, 7, 25, 14, 30, 0) - agent.last_seen = sample_date - agent.diagnostics() - assert "last_seen: 2022-07-25 14:30:00" in caplog.messages - assert "announce_data: None" in caplog.messages - - agent.announce_data = AnnounceData(pid=1234, agentUuid="value") - agent.diagnostics() - assert f"announce_data: {agent.announce_data.__dict__}" in caplog.messages - assert f"Options: {agent.options.__dict__}" in caplog.messages - assert "----> StateMachine <----" in caplog.messages - assert f"State: {agent.machine.fsm.current}" in caplog.messages - assert "----> Collector <----" in caplog.messages - assert f"Collector: {agent.collector}" in caplog.messages - assert f"ready_to_start: {agent.collector.ready_to_start}" in caplog.messages - assert "reporting_thread: None" in caplog.messages - assert f"report_interval: {agent.collector.report_interval}" in caplog.messages - assert "should_send_snapshot_data: True" in caplog.messages + mock_response.status_code = 404 + with patch.object( + requests.Session, "put", return_value=mock_response, clear=True + ): + assert not agent.announce("sample-data") + assert "announce: response status code (404) is NOT 200" in caplog.messages + + def test_log_message_to_host_agent( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = "sample" + mock_datetime = datetime.datetime(2022, 1, 1, 12, 0, 0) + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.datetime" + ) as mock_date: + mock_date.now.return_value = mock_datetime + mock_date.side_effect = lambda *args, **kwargs: datetime(*args, **kwargs) + agent.log_message_to_host_agent("sample") + assert agent.last_seen == mock_datetime + + with patch.object(requests.Session, "post", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.log_message_to_host_agent("sample") + assert ( + f"agent logging: connection error ({type(Exception())})" + in caplog.messages + ) + + def test_is_agent_ready( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + agent = HostAgent() + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = {"key": "value"} + agent.AGENT_DATA_PATH = "sample_path" + agent.announce_data = AnnounceData(pid=1234, agentUuid="sample") + with patch.object(requests.Session, "head", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", + return_value="localhost", + ): + assert agent.is_agent_ready() + with patch.object(requests.Session, "head", side_effect=Exception()): + caplog.set_level(logging.DEBUG, logger="instana") + agent.is_agent_ready() + assert ( + f"is_agent_ready: connection error ({type(Exception())})" + in caplog.messages + ) + + def test_report_data_payload( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + agent = HostAgent() + span_name = "test-span" + span_1 = InstanaSpan(span_name, span_context, span_processor) + span_2 = InstanaSpan(span_name, span_context, span_processor) + payload = { + "spans": [span_1, span_2], + "profiles": ["profile-1", "profile-2"], + "metrics": { + "plugins": [ + {"data": "sample data"}, + ] + }, + } + sample_response = {"key": "value"} + mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = sample_response + with patch.object(requests.Session, "post", return_value=mock_response), patch( + "instana.agent.host.HostAgent._HostAgent__traces_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__profiles_url", + return_value="localhost", + ), patch( + "instana.agent.host.HostAgent._HostAgent__data_url", + return_value="localhost", + ): + test_response = agent.report_data_payload(payload) + assert isinstance(agent.last_seen, datetime.datetime) + assert test_response.content == sample_response + + def test_diagnostics(self, caplog: pytest.LogCaptureFixture) -> None: + caplog.set_level(logging.WARNING, logger="instana") + + agent = HostAgent() + agent.diagnostics() + assert ( + "====> Instana Python Language Agent Diagnostics <====" in caplog.messages + ) + assert "----> Agent <----" in caplog.messages + assert f"is_agent_ready: {agent.is_agent_ready()}" in caplog.messages + assert f"is_timed_out: {agent.is_timed_out()}" in caplog.messages + assert "last_seen: None" in caplog.messages + + sample_date = datetime.datetime(2022, 7, 25, 14, 30, 0) + agent.last_seen = sample_date + agent.diagnostics() + assert "last_seen: 2022-07-25 14:30:00" in caplog.messages + assert "announce_data: None" in caplog.messages + + agent.announce_data = AnnounceData(pid=1234, agentUuid="value") + agent.diagnostics() + assert f"announce_data: {agent.announce_data.__dict__}" in caplog.messages + assert f"Options: {agent.options.__dict__}" in caplog.messages + assert "----> StateMachine <----" in caplog.messages + assert f"State: {agent.machine.fsm.current}" in caplog.messages + assert "----> Collector <----" in caplog.messages + assert f"Collector: {agent.collector}" in caplog.messages + assert f"ready_to_start: {agent.collector.ready_to_start}" in caplog.messages + assert "reporting_thread: None" in caplog.messages + assert f"report_interval: {agent.collector.report_interval}" in caplog.messages + assert "should_send_snapshot_data: True" in caplog.messages diff --git a/tests/collector/helpers/test_collector_runtime.py b/tests/collector/helpers/test_collector_runtime.py new file mode 100644 index 000000000..3717f5f90 --- /dev/null +++ b/tests/collector/helpers/test_collector_runtime.py @@ -0,0 +1,68 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import Generator +from unittest.mock import patch + +import pytest + +from instana.agent.host import HostAgent +from instana.collector.helpers.runtime import RuntimeHelper +from instana.collector.host import HostCollector + + +class TestRuntimeHelper: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.helper = RuntimeHelper( + collector=HostCollector( + HostAgent(), + ), + ) + yield + self.helper = None + + def test_default_while_gc_disabled(self) -> None: + import gc + + gc.disable() + helper = RuntimeHelper(collector=HostCollector(HostAgent())) + assert helper.previous_gc_count is None + + def test_collect_metrics(self) -> None: + response = self.helper.collect_metrics() + assert response[0]["name"] == "com.instana.plugin.python" + + def test_collect_runtime_snapshot_default(self) -> None: + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "Manual" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_runtime_snapshot_autowrapt(self) -> None: + with patch( + "instana.collector.helpers.runtime.is_autowrapt_instrumented", + return_value=True, + ): + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "Autowrapt" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_runtime_snapshot_webhook(self) -> None: + with patch( + "instana.collector.helpers.runtime.is_webhook_instrumented", + return_value=True, + ): + plugin_data = self.helper.collect_metrics() + self.helper._collect_runtime_snapshot(plugin_data[0]) + assert plugin_data[0]["name"] == "com.instana.plugin.python" + assert plugin_data[0]["data"]["snapshot"]["m"] == "AutoTrace" + assert len(plugin_data[0]["data"]) == 3 + + def test_collect_gc_metrics(self) -> None: + plugin_data = self.helper.collect_metrics() + + self.helper._collect_gc_metrics(plugin_data[0], True) + assert len(self.helper.previous["data"]["metrics"]["gc"]) == 6 diff --git a/tests/collector/test_base_collector.py b/tests/collector/test_base_collector.py new file mode 100644 index 000000000..8f8550a87 --- /dev/null +++ b/tests/collector/test_base_collector.py @@ -0,0 +1,214 @@ +# (c) Copyright IBM Corp. 2024 + +import logging +import queue +import threading +import time +from typing import Generator +from unittest.mock import patch + +import pytest +from pytest import LogCaptureFixture + +from instana.agent.host import HostAgent +from instana.collector.base import BaseCollector +from instana.recorder import StanRecorder +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan +from instana.span_context import SpanContext + + +class TestBaseCollector: + @pytest.fixture(autouse=True) + def _resource( + self, + caplog: LogCaptureFixture, + ) -> Generator[None, None, None]: + self.collector = BaseCollector(HostAgent()) + yield + self.collector.shutdown(report_final=False) + self.collector = None + caplog.clear() + + def test_default(self) -> None: + assert isinstance(self.collector.agent, HostAgent) + assert self.collector.THREAD_NAME == "Instana Collector" + assert isinstance(self.collector.span_queue, queue.Queue) + assert isinstance(self.collector.profile_queue, queue.Queue) + assert not self.collector.reporting_thread + assert isinstance(self.collector.thread_shutdown, threading.Event) + assert self.collector.snapshot_data_last_sent == 0 + assert self.collector.snapshot_data_interval == 300 + assert len(self.collector.helpers) == 0 + assert self.collector.report_interval == 1 + assert not self.collector.started + assert self.collector.fetching_start_time == 0 + + def test_is_reporting_thread_running(self) -> None: + stop_event = threading.Event() + + def reporting_function(): + stop_event.wait() + + sample_thread = threading.Thread( + name=self.collector.THREAD_NAME, target=reporting_function + ) + sample_thread.start() + try: + assert self.collector.is_reporting_thread_running() + finally: + stop_event.set() + sample_thread.join() + + def test_is_reporting_thread_running_with_different_name(self) -> None: + self.collector.THREAD_NAME = "sample-collector" + stop_event = threading.Event() + + def reporting_function(): + stop_event.wait() + + sample_thread = threading.Thread(name="test-thread", target=reporting_function) + sample_thread.start() + try: + assert not self.collector.is_reporting_thread_running() + finally: + stop_event.set() + sample_thread.join() + + def test_start_collector_while_running_thread( + self, + caplog: LogCaptureFixture, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=True, + ): + self.collector.start() + assert ( + "BaseCollector.start non-fatal: call but thread already running (started: False)" + in caplog.messages + ) + + def test_start_agent_shutdown_is_set(self) -> None: + self.collector.thread_shutdown.set() + isThreadFound = False + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=True, + ): + response = self.collector.start() + assert not response + for thread in threading.enumerate(): + if thread.name == "Collector Timed Start": + isThreadFound = True + assert isThreadFound + + def test_start_collector_when_agent_is_ready( + self, + caplog: LogCaptureFixture, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=False, + ): + if not self.collector.started: + self.collector.start() + assert self.collector.started + assert self.collector.reporting_thread.daemon + assert ( + self.collector.reporting_thread.name == self.collector.THREAD_NAME + ) + + def test_start_agent_can_not_send( + self, + caplog: LogCaptureFixture, + ) -> None: + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=False, + ), patch("instana.agent.host.HostAgent.can_send", return_value=False): + caplog.set_level(logging.WARNING, logger="instana") + self.collector.agent.machine.fsm.current = "test" + self.collector.start() + assert ( + "BaseCollector.start: the agent tells us we can't send anything out" + in caplog.messages + ) + + def test_shutdown( + self, + caplog: LogCaptureFixture, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.shutdown() + assert "Collector.shutdown: Reporting final data." in caplog.messages + assert not self.collector.started + + def test_background_report(self) -> None: + assert self.collector.background_report() + self.collector.thread_shutdown.set() + assert not self.collector.background_report() + + def test_should_send_snapshot_data(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.should_send_snapshot_data() + assert ( + "BaseCollector: should_send_snapshot_data needs to be overridden" + in caplog.messages + ) + + def test_collect_snapshot( + self, + caplog: LogCaptureFixture, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.collector.collect_snapshot() + assert ( + "BaseCollector: collect_snapshot needs to be overridden" in caplog.messages + ) + + def test_queued_spans( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + for span in span_list: + self.collector.span_queue.put(span) + time.sleep(0.1) + spans = self.collector.queued_spans() + assert len(spans) == 3 + + def test_queued_profiles( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_list = [ + RegisteredSpan( + InstanaSpan("span1", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span2", span_context, span_processor), None, "log" + ), + RegisteredSpan( + InstanaSpan("span3", span_context, span_processor), None, "log" + ), + ] + for span in span_list: + self.collector.profile_queue.put(span) + time.sleep(0.1) + profiles = self.collector.queued_profiles() + assert len(profiles) == 3 diff --git a/tests/platforms/test_gcr_collector.py b/tests/collector/test_gcr_collector.py similarity index 100% rename from tests/platforms/test_gcr_collector.py rename to tests/collector/test_gcr_collector.py diff --git a/tests/collector/test_host_collector.py b/tests/collector/test_host_collector.py new file mode 100644 index 000000000..fe0e4953d --- /dev/null +++ b/tests/collector/test_host_collector.py @@ -0,0 +1,280 @@ +# (c) Copyright IBM Corp. 2021 +# (c) Copyright Instana Inc. 2020 + +import gc +import logging +import os +import sys +import threading +from typing import Generator + +import pytest +from instana.collector.helpers.runtime import ( + PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR, +) +from instana.collector.host import HostCollector +from instana.singletons import get_agent, get_tracer +from instana.version import VERSION +from mock import patch +from pytest import LogCaptureFixture + + +class TestHostCollector: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.agent = get_agent() + self.agent.collector = HostCollector(self.agent) + self.tracer = get_tracer() + self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + "3.8.0" + self.payload = None + yield + self.agent.collector.shutdown(report_final=False) + variable_names = ( + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_AGENT_KEY", + "INSTANA_ZONE", + "INSTANA_TAGS", + "INSTANA_DISABLE_METRICS_COLLECTION", + "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", + "AUTOWRAPT_BOOTSTRAP", + ) + + for variable_name in variable_names: + if variable_name in os.environ: + os.environ.pop(variable_name) + + if self.webhook_sitedir_path in sys.path: + sys.path.remove(self.webhook_sitedir_path) + + def test_start(self) -> None: + with patch( + "instana.collector.base.BaseCollector.is_reporting_thread_running", + return_value=False, + ): + self.agent.collector.start() + assert self.agent.collector.started + assert self.agent.collector.THREAD_NAME == "Instana Collector" + assert self.agent.collector.snapshot_data_interval == 300 + assert self.agent.collector.snapshot_data_last_sent == 0 + assert isinstance(self.agent.collector.helpers[0].collector, HostCollector) + assert len(self.agent.collector.helpers) == 1 + assert isinstance(self.agent.collector.reporting_thread, threading.Thread) + self.agent.collector.ready_to_start = False + assert not self.agent.collector.start() + + def test_prepare_and_report_data(self, caplog: LogCaptureFixture) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + self.agent.collector.agent.machine.fsm.current = "wait4init" + with patch("instana.agent.host.HostAgent.is_agent_ready", return_value=True): + self.agent.collector.prepare_and_report_data() + assert "Agent is ready. Getting to work." in caplog.messages + assert "Harmless state machine thread disagreement. Will self-correct on next timer cycle." + self.agent.collector.agent.machine.fsm.current = "wait4init" + with patch("instana.agent.host.HostAgent.is_agent_ready", return_value=False): + assert not self.agent.collector.prepare_and_report_data() + self.agent.collector.agent.machine.fsm.current = "good2go" + caplog.clear() + with patch("instana.agent.host.HostAgent.is_timed_out", return_value=True): + self.agent.collector.prepare_and_report_data() + assert ( + "The Instana host agent has gone offline or is no longer reachable for > 1 min. Will retry periodically." + in caplog.messages + ) + + def test_should_send_snapshot_data(self) -> None: + self.agent.collector.snapshot_data_interval = 999999999999 + assert not self.agent.collector.should_send_snapshot_data() + + def test_prepare_payload_basics(self) -> None: + with patch.object(gc, "isenabled", return_value=True): + self.payload = self.agent.collector.prepare_payload() + assert self.payload + + assert len(self.payload.keys()) == 3 + assert "spans" in self.payload + assert isinstance(self.payload["spans"], list) + assert len(self.payload["spans"]) == 0 + assert "metrics", self.payload + assert len(self.payload["metrics"].keys()) == 1 + assert "plugins", self.payload["metrics"] + assert isinstance(self.payload["metrics"]["plugins"], list) + assert len(self.payload["metrics"]["plugins"]) == 1 + + python_plugin = self.payload["metrics"]["plugins"][0] + assert python_plugin["name"] == "com.instana.plugin.python" + assert python_plugin["entityId"] == str(os.getpid()) + assert "data" in python_plugin + assert "snapshot" in python_plugin["data"] + assert "m" in python_plugin["data"]["snapshot"] + assert "Manual" == python_plugin["data"]["snapshot"]["m"] + assert "metrics" in python_plugin["data"] + + assert "ru_utime" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_utime"]) in [float, int] + assert "ru_stime" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_stime"]) in [float, int] + assert "ru_maxrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_maxrss"]) in [float, int] + assert "ru_ixrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_ixrss"]) in [float, int] + assert "ru_idrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_idrss"]) in [float, int] + assert "ru_isrss" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_isrss"]) in [float, int] + assert "ru_minflt" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_minflt"]) in [float, int] + assert "ru_majflt" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_majflt"]) in [float, int] + assert "ru_nswap" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nswap"]) in [float, int] + assert "ru_inblock" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_inblock"]) in [float, int] + assert "ru_oublock" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_oublock"]) in [float, int] + assert "ru_msgsnd" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_msgsnd"]) in [float, int] + assert "ru_msgrcv" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_msgrcv"]) in [float, int] + assert "ru_nsignals" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nsignals"]) in [float, int] + assert "ru_nvcsw" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nvcsw"]) in [float, int] + assert "ru_nivcsw" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["ru_nivcsw"]) in [float, int] + assert "alive_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["alive_threads"]) in [ + float, + int, + ] + assert "dummy_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["dummy_threads"]) in [ + float, + int, + ] + assert "daemon_threads" in python_plugin["data"]["metrics"] + assert type(python_plugin["data"]["metrics"]["daemon_threads"]) in [ + float, + int, + ] + + assert "gc" in python_plugin["data"]["metrics"] + assert isinstance(python_plugin["data"]["metrics"]["gc"], dict) + assert "collect0" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect0"]) in [ + float, + int, + ] + assert "collect1" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect1"]) in [ + float, + int, + ] + assert "collect2" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["collect2"]) in [ + float, + int, + ] + assert "threshold0" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold0"]) in [ + float, + int, + ] + assert "threshold1" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold1"]) in [ + float, + int, + ] + assert "threshold2" in python_plugin["data"]["metrics"]["gc"] + assert type(python_plugin["data"]["metrics"]["gc"]["threshold2"]) in [ + float, + int, + ] + + def test_prepare_payload_basics_disable_runtime_metrics(self) -> None: + os.environ["INSTANA_DISABLE_METRICS_COLLECTION"] = "TRUE" + self.payload = self.agent.collector.prepare_payload() + assert self.payload + + assert len(self.payload.keys()) == 3 + assert "spans" in self.payload + assert isinstance(self.payload["spans"], list) + assert len(self.payload["spans"]) == 0 + assert "metrics" in self.payload + assert len(self.payload["metrics"].keys()) == 1 + assert "plugins" in self.payload["metrics"] + assert isinstance(self.payload["metrics"]["plugins"], list) + assert len(self.payload["metrics"]["plugins"]) == 1 + + python_plugin = self.payload["metrics"]["plugins"][0] + assert python_plugin["name"] == "com.instana.plugin.python" + assert python_plugin["entityId"] == str(os.getpid()) + assert "data" in python_plugin + assert "snapshot" in python_plugin["data"] + assert "m" in python_plugin["data"]["snapshot"] + assert "Manual" == python_plugin["data"]["snapshot"]["m"] + assert "metrics" not in python_plugin["data"] + + def test_prepare_payload_with_snapshot_with_python_packages(self) -> None: + self.payload = self.agent.collector.prepare_payload() + assert self.payload + assert "snapshot" in self.payload["metrics"]["plugins"][0]["data"] + snapshot = self.payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Manual" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + assert snapshot["versions"]["instana"] == VERSION + assert "wrapt" in snapshot["versions"] + assert "fysom" in snapshot["versions"] + + def test_prepare_payload_with_snapshot_disabled_python_packages(self) -> None: + os.environ["INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"] = "TRUE" + self.payload = self.agent.collector.prepare_payload() + assert self.payload + assert "snapshot" in self.payload["metrics"]["plugins"][0]["data"] + snapshot = self.payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Manual" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) == 1 + assert snapshot["versions"]["instana"] == VERSION + + def test_prepare_payload_with_autowrapt(self) -> None: + os.environ["AUTOWRAPT_BOOTSTRAP"] = "instana" + self.payload = self.agent.collector.prepare_payload() + assert self.payload + assert "snapshot" in self.payload["metrics"]["plugins"][0]["data"] + snapshot = self.payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "Autowrapt" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + expected_packages = ("instana", "wrapt", "fysom") + for package in expected_packages: + assert ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" + assert snapshot["versions"]["instana"] == VERSION + + def test_prepare_payload_with_autotrace(self) -> None: + sys.path.append(self.webhook_sitedir_path) + self.payload = self.agent.collector.prepare_payload() + assert self.payload + assert "snapshot" in self.payload["metrics"]["plugins"][0]["data"] + snapshot = self.payload["metrics"]["plugins"][0]["data"]["snapshot"] + assert snapshot + assert "m" in snapshot + assert "AutoTrace" == snapshot["m"] + assert "version" in snapshot + assert len(snapshot["versions"]) > 5 + expected_packages = ("instana", "wrapt", "fysom") + for package in expected_packages: + assert ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" + assert snapshot["versions"]["instana"] == VERSION diff --git a/tests/collector/test_utils.py b/tests/collector/test_utils.py new file mode 100644 index 000000000..373e31490 --- /dev/null +++ b/tests/collector/test_utils.py @@ -0,0 +1,47 @@ +import time +import pytest +from typing import Generator +from instana.collector.utils import format_span +from instana.singletons import tracer +from instana.span.registered_span import RegisteredSpan +from instana.span.span import InstanaSpan, get_current_span +from opentelemetry.trace.span import format_span_id +from opentelemetry.trace import SpanKind + +from instana.span_context import SpanContext + + +class TestUtils: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.recorder = tracer.span_processor + self.span_context = None + yield + + def test_format_span(self, span_context: SpanContext) -> None: + self.span_context = span_context + with tracer.start_as_current_span( + name="span1", span_context=self.span_context + ) as pspan: + expected_trace_id = format_span_id(pspan.context.trace_id) + expected_span_id = format_span_id(pspan.context.span_id) + assert get_current_span() is pspan + with tracer.start_as_current_span(name="span2") as cspan: + assert get_current_span() is cspan + assert cspan.parent_id == pspan.context.span_id + span_list = [ + RegisteredSpan(pspan, None, "log"), + RegisteredSpan(cspan, None, "log"), + ] + formatted_spans = format_span(span_list) + assert len(formatted_spans) == 2 + assert formatted_spans[0].t == expected_trace_id + assert formatted_spans[0].k == 1 + assert formatted_spans[0].s == expected_span_id + assert formatted_spans[0].n == "span1" + + assert formatted_spans[1].t == expected_trace_id + assert formatted_spans[1].p == formatted_spans[0].s + assert formatted_spans[1].k == 1 + assert formatted_spans[1].s != formatted_spans[0].s + assert formatted_spans[1].n == "span2" diff --git a/tests/conftest.py b/tests/conftest.py index c9e11688a..00231691c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,8 +26,8 @@ collect_ignore_glob = [ "*test_gevent*", - "*platforms/test_gcr*", - "*platforms/test_google*", + "*collector/test_gcr*", + "*agent/test_google*", ] # # Cassandra and gevent tests are run in dedicated jobs on CircleCI and will diff --git a/tests/platforms/test_google_cloud_run.py b/tests/platforms/test_google_cloud_run.py deleted file mode 100644 index 8b086a708..000000000 --- a/tests/platforms/test_google_cloud_run.py +++ /dev/null @@ -1,129 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2021 - -import os -import logging -import unittest - -from instana.tracer import InstanaTracer -from instana.options import GCROptions -from instana.recorder import StanRecorder -from instana.agent.google_cloud_run import GCRAgent -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer - - -class TestGCR(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestGCR, self).__init__(methodName) - self.agent = None - self.span_recorder = None - self.tracer = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - os.environ["K_SERVICE"] = "service" - os.environ["K_CONFIGURATION"] = "configuration" - os.environ["K_REVISION"] = "revision" - os.environ["PORT"] = "port" - os.environ["INSTANA_ENDPOINT_URL"] = "https://localhost/notreal" - os.environ["INSTANA_AGENT_KEY"] = "Fake_Key" - - def tearDown(self): - """ Reset all environment variables of consequence """ - if "K_SERVICE" in os.environ: - os.environ.pop("K_SERVICE") - if "K_CONFIGURATION" in os.environ: - os.environ.pop("K_CONFIGURATION") - if "K_REVISION" in os.environ: - os.environ.pop("K_REVISION") - if "PORT" in os.environ: - os.environ.pop("PORT") - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_ENDPOINT_PROXY" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_PROXY") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - if "INSTANA_LOG_LEVEL" in os.environ: - os.environ.pop("INSTANA_LOG_LEVEL") - if "INSTANA_SECRETS" in os.environ: - os.environ.pop("INSTANA_SECRETS") - if "INSTANA_DEBUG" in os.environ: - os.environ.pop("INSTANA_DEBUG") - if "INSTANA_TAGS" in os.environ: - os.environ.pop("INSTANA_TAGS") - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = GCRAgent(service="service", configuration="configuration", revision="revision") - self.span_recorder = StanRecorder(self.agent) - self.tracer = InstanaTracer(recorder=self.span_recorder) - set_agent(self.agent) - set_tracer(self.tracer) - - def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, GCROptions)) - - def test_invalid_options(self): - # None of the required env vars are available... - if "INSTANA_EXTRA_HTTP_HEADERS" in os.environ: - os.environ.pop("INSTANA_EXTRA_HTTP_HEADERS") - if "INSTANA_ENDPOINT_URL" in os.environ: - os.environ.pop("INSTANA_ENDPOINT_URL") - if "INSTANA_AGENT_KEY" in os.environ: - os.environ.pop("INSTANA_AGENT_KEY") - - agent = GCRAgent(service="service", configuration="configuration", revision="revision") - self.assertFalse(agent.can_send()) - self.assertIsNone(agent.collector) - - def test_default_secrets(self): - self.create_agent_and_setup_tracer() - self.assertIsNone(self.agent.options.secrets) - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) - - def test_custom_secrets(self): - os.environ["INSTANA_SECRETS"] = "equals:love,war,games" - self.create_agent_and_setup_tracer() - - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'equals') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['love', 'war', 'games']) - - def test_has_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) - - def test_agent_extra_http_headers(self): - os.environ['INSTANA_EXTRA_HTTP_HEADERS'] = "X-Test-Header;X-Another-Header;X-And-Another-Header" - self.create_agent_and_setup_tracer() - self.assertIsNotNone(self.agent.options.extra_http_headers) - should_headers = ['x-test-header', 'x-another-header', 'x-and-another-header'] - self.assertEqual(should_headers, self.agent.options.extra_http_headers) - - def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - assert self.agent.options.log_level == logging.WARNING - - def test_agent_custom_log_level(self): - os.environ['INSTANA_LOG_LEVEL'] = "eRror" - self.create_agent_and_setup_tracer() - assert self.agent.options.log_level == logging.ERROR - - def test_custom_proxy(self): - os.environ["INSTANA_ENDPOINT_PROXY"] = "http://myproxy.123" - self.create_agent_and_setup_tracer() - assert self.agent.options.endpoint_proxy == {'https': "http://myproxy.123"} diff --git a/tests/platforms/test_host.py b/tests/platforms/test_host.py index fcfc80a9a..75cea793f 100644 --- a/tests/platforms/test_host.py +++ b/tests/platforms/test_host.py @@ -1,270 +1,233 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2020 -import os import logging -import unittest -import pytest +import os +from typing import Generator -from mock import MagicMock, patch +import pytest import requests +from mock import MagicMock, patch -from instana.agent.host import HostAgent from instana.fsm import Discovery -from instana.log import logger from instana.options import StandardOptions -from instana.recorder import StanRecorder -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer +from instana.singletons import get_agent -class TestHost(unittest.TestCase): - def __init__(self, methodName='runTest'): - super(TestHost, self).__init__(methodName) - self.agent = None - self.span_recorder = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): +class TestHost: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.agent = get_agent() + self.span_processor = None + self.agent.options = StandardOptions() pass - - def tearDown(self): - """ Reset all environment variables of consequence """ variable_names = ( - "AWS_EXECUTION_ENV", "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", "INSTANA_ENDPOINT_PROXY", - "INSTANA_AGENT_KEY", "INSTANA_LOG_LEVEL", - "INSTANA_SERVICE_NAME", "INSTANA_SECRETS", "INSTANA_TAGS", - ) + "AWS_EXECUTION_ENV", + "INSTANA_EXTRA_HTTP_HEADERS", + "INSTANA_ENDPOINT_URL", + "INSTANA_ENDPOINT_PROXY", + "INSTANA_AGENT_KEY", + "INSTANA_LOG_LEVEL", + "INSTANA_SERVICE_NAME", + "INSTANA_SECRETS", + "INSTANA_TAGS", + ) for variable_name in variable_names: if variable_name in os.environ: os.environ.pop(variable_name) - set_agent(self.original_agent) - set_tracer(self.original_tracer) - - def create_agent_and_setup_tracer(self): - self.agent = HostAgent() - self.span_recorder = StanRecorder(self.agent) - set_agent(self.agent) - def test_secrets(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent.options, 'secrets_matcher')) - self.assertEqual(self.agent.options.secrets_matcher, 'contains-ignore-case') - self.assertTrue(hasattr(self.agent.options, 'secrets_list')) - self.assertEqual(self.agent.options.secrets_list, ['key', 'pass', 'secret']) + assert hasattr(self.agent.options, "secrets_matcher") + assert self.agent.options.secrets_matcher == "contains-ignore-case" + assert hasattr(self.agent.options, "secrets_list") + assert self.agent.options.secrets_list == ["key", "pass", "secret"] def test_options_have_extra_http_headers(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(hasattr(self.agent.options, 'extra_http_headers')) + assert hasattr(self.agent, "options") + assert hasattr(self.agent.options, "extra_http_headers") def test_has_options(self): - self.create_agent_and_setup_tracer() - self.assertTrue(hasattr(self.agent, 'options')) - self.assertTrue(isinstance(self.agent.options, StandardOptions)) + assert hasattr(self.agent, "options") + assert isinstance(self.agent.options, StandardOptions) def test_agent_default_log_level(self): - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.WARNING) + assert self.agent.options.log_level == logging.DEBUG def test_agent_instana_debug(self): - os.environ['INSTANA_DEBUG'] = "asdf" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.log_level, logging.DEBUG) + os.environ["INSTANA_DEBUG"] = "asdf" + self.agent.options = StandardOptions() + assert self.agent.options.log_level == logging.DEBUG def test_agent_instana_service_name(self): - os.environ['INSTANA_SERVICE_NAME'] = "greycake" - self.create_agent_and_setup_tracer() - self.assertEqual(self.agent.options.service_name, "greycake") + os.environ["INSTANA_SERVICE_NAME"] = "greycake" + self.agent.options = StandardOptions() + assert self.agent.options.service_name == "greycake" @patch.object(requests.Session, "put") def test_announce_is_successful(self, mock_requests_session_put): test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" mock_response = MagicMock() mock_response.status_code = 200 mock_response.content = ( - '{' - f' "pid": {test_pid}, ' - f' "agentUuid": "{test_agent_uuid}"' - '}') + "{" f' "pid": {test_pid}, ' f' "agentUuid": "{test_agent_uuid}"' "}" + ) # This mocks the call to self.agent.client.put mock_requests_session_put.return_value = mock_response - - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) payload = self.agent.announce(d) - self.assertIn('pid', payload) - self.assertEqual(test_pid, payload['pid']) - - self.assertIn('agentUuid', payload) - self.assertEqual(test_agent_uuid, payload['agentUuid']) + assert "pid" in payload + assert test_pid == payload["pid"] + assert "agentUuid" in payload + assert test_agent_uuid == payload["agentUuid"] @patch.object(requests.Session, "put") - def test_announce_fails_with_non_200(self, mock_requests_session_put): + def test_announce_fails_with_non_200(self, mock_requests_session_put, caplog): + caplog.set_level(logging.DEBUG, logger="instana") test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" mock_response = MagicMock() mock_response.status_code = 404 - mock_response.content = '' + mock_response.content = "" mock_requests_session_put.return_value = mock_response - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response status code', log.output[0]) - self.assertIn('is NOT 200', log.output[0]) + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) + assert not payload + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response status code" in caplog.messages[0] + assert "is NOT 200" in caplog.messages[0] @patch.object(requests.Session, "put") - def test_announce_fails_with_non_json(self, mock_requests_session_put): + def test_announce_fails_with_non_json(self, mock_requests_session_put, caplog): + caplog.set_level(logging.DEBUG, logger="instana") test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" mock_response = MagicMock() mock_response.status_code = 200 - mock_response.content = '' + mock_response.content = "" mock_requests_session_put.return_value = mock_response - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response is not JSON', log.output[0]) + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) + assert not payload + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response is not JSON" in caplog.messages[0] @patch.object(requests.Session, "put") - def test_announce_fails_with_empty_list_json(self, mock_requests_session_put): + def test_announce_fails_with_empty_list_json( + self, mock_requests_session_put, caplog + ): + caplog.set_level(logging.DEBUG, logger="instana") test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" mock_response = MagicMock() mock_response.status_code = 200 - mock_response.content = '[]' + mock_response.content = "[]" mock_requests_session_put.return_value = mock_response - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('payload has no fields', log.output[0]) - + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) + assert not payload + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "payload has no fields" in caplog.messages[0] @patch.object(requests.Session, "put") - def test_announce_fails_with_missing_pid(self, mock_requests_session_put): + def test_announce_fails_with_missing_pid(self, mock_requests_session_put, caplog): + caplog.set_level(logging.DEBUG, logger="instana") test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" mock_response = MagicMock() mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "agentUuid": "{test_agent_uuid}"' - '}') + mock_response.content = "{" f' "agentUuid": "{test_agent_uuid}"' "}" mock_requests_session_put.return_value = mock_response - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response payload has no pid', log.output[0]) - + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) + assert not payload + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no pid" in caplog.messages[0] @patch.object(requests.Session, "put") - def test_announce_fails_with_missing_uuid(self, mock_requests_session_put): + def test_announce_fails_with_missing_uuid(self, mock_requests_session_put, caplog): + caplog.set_level(logging.DEBUG, logger="instana") test_pid = 4242 - test_process_name = 'test_process' - test_process_args = ['-v', '-d'] - test_agent_uuid = '83bf1e09-ab16-4203-abf5-34ee0977023a' + test_process_name = "test_process" + test_process_args = ["-v", "-d"] + test_agent_uuid = "83bf1e09-ab16-4203-abf5-34ee0977023a" mock_response = MagicMock() mock_response.status_code = 200 - mock_response.content = ( - '{' - f' "pid": {test_pid} ' - '}') + mock_response.content = "{" f' "pid": {test_pid} ' "}" mock_requests_session_put.return_value = mock_response - self.create_agent_and_setup_tracer() - d = Discovery(pid=test_pid, - name=test_process_name, args=test_process_args) - with self.assertLogs(logger, level='DEBUG') as log: - payload = self.agent.announce(d) - self.assertIsNone(payload) - self.assertEqual(len(log.output), 1) - self.assertEqual(len(log.records), 1) - self.assertIn('response payload has no agentUuid', log.output[0]) + d = Discovery(pid=test_pid, name=test_process_name, args=test_process_args) + payload = self.agent.announce(d) + assert not payload + assert len(caplog.messages) == 1 + assert len(caplog.records) == 1 + assert "response payload has no agentUuid" in caplog.messages[0] @pytest.mark.original @patch.object(requests.Session, "get") - def test_agent_connection_attempt(self, mock_requests_session_get): + def test_agent_connection_attempt(self, mock_requests_session_get, caplog): + caplog.set_level(logging.DEBUG, logger="instana") mock_response = MagicMock() mock_response.status_code = 200 mock_requests_session_get.return_value = mock_response - self.create_agent_and_setup_tracer() host = self.agent.options.agent_host port = self.agent.options.agent_port msg = f"Instana host agent found on {host}:{port}" - - with self.assertLogs(logger, level='DEBUG') as log: - result = self.agent.is_agent_listening(host, port) - self.assertTrue(result) - self.assertIn(msg, log.output[0]) + result = self.agent.is_agent_listening(host, port) + + assert result + assert msg in caplog.messages[0] @pytest.mark.original @patch.object(requests.Session, "get") - def test_agent_connection_attempt_fails_with_404(self, mock_requests_session_get): + def test_agent_connection_attempt_fails_with_404( + self, mock_requests_session_get, caplog + ): + caplog.set_level(logging.DEBUG, logger="instana") mock_response = MagicMock() mock_response.status_code = 404 mock_requests_session_get.return_value = mock_response - self.create_agent_and_setup_tracer() host = self.agent.options.agent_host port = self.agent.options.agent_port - msg = "The attempt to connect to the Instana host agent on " \ - f"{host}:{port} has failed with an unexpected status code. " \ - f"Expected HTTP 200 but received: {mock_response.status_code}" + msg = ( + "The attempt to connect to the Instana host agent on " + f"{host}:{port} has failed with an unexpected status code. " + f"Expected HTTP 200 but received: {mock_response.status_code}" + ) - with self.assertLogs(logger, level='DEBUG') as log: - result = self.agent.is_agent_listening(host, port) + result = self.agent.is_agent_listening(host, port) - self.assertFalse(result) - self.assertIn(msg, log.output[0]) + assert not result + assert msg in caplog.messages[0] diff --git a/tests/platforms/test_host_collector.py b/tests/platforms/test_host_collector.py deleted file mode 100644 index 48b6fd3d4..000000000 --- a/tests/platforms/test_host_collector.py +++ /dev/null @@ -1,275 +0,0 @@ -# (c) Copyright IBM Corp. 2021 -# (c) Copyright Instana Inc. 2020 - -import os -import unittest -import sys - -from mock import patch - -from instana.recorder import StanRecorder -from instana.agent.host import HostAgent -from instana.collector.helpers.runtime import PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR -from instana.collector.host import HostCollector -from instana.singletons import get_agent, set_agent, get_tracer, set_tracer -from instana.version import VERSION - - -class TestHostCollector(unittest.TestCase): - def __init__(self, methodName="runTest"): - super(TestHostCollector, self).__init__(methodName) - self.agent = None - self.span_recorder = None - - self.original_agent = get_agent() - self.original_tracer = get_tracer() - - def setUp(self): - self.webhook_sitedir_path = PATH_OF_AUTOTRACE_WEBHOOK_SITEDIR + '3.8.0' - - def tearDown(self): - """Reset all environment variables of consequence""" - variable_names = ( - "AWS_EXECUTION_ENV", - "INSTANA_EXTRA_HTTP_HEADERS", - "INSTANA_ENDPOINT_URL", - "INSTANA_AGENT_KEY", - "INSTANA_ZONE", - "INSTANA_TAGS", - "INSTANA_DISABLE_METRICS_COLLECTION", - "INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION", - "AUTOWRAPT_BOOTSTRAP", - ) - - for variable_name in variable_names: - if variable_name in os.environ: - os.environ.pop(variable_name) - - set_agent(self.original_agent) - set_tracer(self.original_tracer) - if self.webhook_sitedir_path in sys.path: - sys.path.remove(self.webhook_sitedir_path) - - def create_agent_and_setup_tracer(self): - self.agent = HostAgent() - self.span_recorder = StanRecorder(self.agent) - set_agent(self.agent) - - def test_prepare_payload_basics(self): - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(len(payload.keys()), 3) - self.assertIn("spans", payload) - self.assertIsInstance(payload["spans"], list) - self.assertEqual(len(payload["spans"]), 0) - self.assertIn("metrics", payload) - self.assertEqual(len(payload["metrics"].keys()), 1) - self.assertIn("plugins", payload["metrics"]) - self.assertIsInstance(payload["metrics"]["plugins"], list) - self.assertEqual(len(payload["metrics"]["plugins"]), 1) - - python_plugin = payload["metrics"]["plugins"][0] - self.assertEqual(python_plugin["name"], "com.instana.plugin.python") - self.assertEqual(python_plugin["entityId"], str(os.getpid())) - self.assertIn("data", python_plugin) - self.assertIn("snapshot", python_plugin["data"]) - self.assertIn("m", python_plugin["data"]["snapshot"]) - self.assertEqual("Manual", python_plugin["data"]["snapshot"]["m"]) - self.assertIn("metrics", python_plugin["data"]) - - # Validate that all metrics are reported on the first run - self.assertIn("ru_utime", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_utime"]), [float, int]) - self.assertIn("ru_stime", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_stime"]), [float, int]) - self.assertIn("ru_maxrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_maxrss"]), [float, int]) - self.assertIn("ru_ixrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_ixrss"]), [float, int]) - self.assertIn("ru_idrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_idrss"]), [float, int]) - self.assertIn("ru_isrss", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_isrss"]), [float, int]) - self.assertIn("ru_minflt", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_minflt"]), [float, int]) - self.assertIn("ru_majflt", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_majflt"]), [float, int]) - self.assertIn("ru_nswap", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_nswap"]), [float, int]) - self.assertIn("ru_inblock", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["ru_inblock"]), [float, int] - ) - self.assertIn("ru_oublock", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["ru_oublock"]), [float, int] - ) - self.assertIn("ru_msgsnd", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_msgsnd"]), [float, int]) - self.assertIn("ru_msgrcv", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_msgrcv"]), [float, int]) - self.assertIn("ru_nsignals", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["ru_nsignals"]), [float, int] - ) - self.assertIn("ru_nvcsw", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_nvcsw"]), [float, int]) - self.assertIn("ru_nivcsw", python_plugin["data"]["metrics"]) - self.assertIn(type(python_plugin["data"]["metrics"]["ru_nivcsw"]), [float, int]) - self.assertIn("alive_threads", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["alive_threads"]), [float, int] - ) - self.assertIn("dummy_threads", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["dummy_threads"]), [float, int] - ) - self.assertIn("daemon_threads", python_plugin["data"]["metrics"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["daemon_threads"]), [float, int] - ) - - self.assertIn("gc", python_plugin["data"]["metrics"]) - self.assertIsInstance(python_plugin["data"]["metrics"]["gc"], dict) - self.assertIn("collect0", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["collect0"]), [float, int] - ) - self.assertIn("collect1", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["collect1"]), [float, int] - ) - self.assertIn("collect2", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["collect2"]), [float, int] - ) - self.assertIn("threshold0", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["threshold0"]), [float, int] - ) - self.assertIn("threshold1", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["threshold1"]), [float, int] - ) - self.assertIn("threshold2", python_plugin["data"]["metrics"]["gc"]) - self.assertIn( - type(python_plugin["data"]["metrics"]["gc"]["threshold2"]), [float, int] - ) - - def test_prepare_payload_basics_disable_runtime_metrics(self): - os.environ["INSTANA_DISABLE_METRICS_COLLECTION"] = "TRUE" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - - self.assertEqual(len(payload.keys()), 3) - self.assertIn("spans", payload) - self.assertIsInstance(payload["spans"], list) - self.assertEqual(len(payload["spans"]), 0) - self.assertIn("metrics", payload) - self.assertEqual(len(payload["metrics"].keys()), 1) - self.assertIn("plugins", payload["metrics"]) - self.assertIsInstance(payload["metrics"]["plugins"], list) - self.assertEqual(len(payload["metrics"]["plugins"]), 1) - - python_plugin = payload["metrics"]["plugins"][0] - self.assertEqual(python_plugin["name"], "com.instana.plugin.python") - self.assertEqual(python_plugin["entityId"], str(os.getpid())) - self.assertIn("data", python_plugin) - self.assertIn("snapshot", python_plugin["data"]) - self.assertIn("m", python_plugin["data"]["snapshot"]) - self.assertEqual("Manual", python_plugin["data"]["snapshot"]["m"]) - self.assertNotIn("metrics", python_plugin["data"]) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_with_python_packages( - self, mock_should_send_snapshot_data - ): - mock_should_send_snapshot_data.return_value = True - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("Manual", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertGreater(len(snapshot["versions"]), 5) - self.assertEqual(snapshot["versions"]["instana"], VERSION) - self.assertIn("wrapt", snapshot["versions"]) - self.assertIn("fysom", snapshot["versions"]) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_snapshot_disabled_python_packages( - self, mock_should_send_snapshot_data - ): - mock_should_send_snapshot_data.return_value = True - os.environ["INSTANA_DISABLE_PYTHON_PACKAGE_COLLECTION"] = "TRUE" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("Manual", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertEqual(len(snapshot["versions"]), 1) - self.assertEqual(snapshot["versions"]["instana"], VERSION) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_autowrapt(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - os.environ["AUTOWRAPT_BOOTSTRAP"] = "instana" - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("Autowrapt", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertGreater(len(snapshot["versions"]), 5) - expected_packages = ("instana", "wrapt", "fysom") - for package in expected_packages: - self.assertIn( - package, - snapshot["versions"], - f"{package} not found in snapshot['versions']", - ) - self.assertEqual(snapshot["versions"]["instana"], VERSION) - - @patch.object(HostCollector, "should_send_snapshot_data") - def test_prepare_payload_with_autotrace(self, mock_should_send_snapshot_data): - mock_should_send_snapshot_data.return_value = True - - sys.path.append(self.webhook_sitedir_path) - - self.create_agent_and_setup_tracer() - - payload = self.agent.collector.prepare_payload() - self.assertTrue(payload) - self.assertIn("snapshot", payload["metrics"]["plugins"][0]["data"]) - snapshot = payload["metrics"]["plugins"][0]["data"]["snapshot"] - self.assertTrue(snapshot) - self.assertIn("m", snapshot) - self.assertEqual("AutoTrace", snapshot["m"]) - self.assertIn("version", snapshot) - self.assertGreater(len(snapshot["versions"]), 5) - expected_packages = ("instana", "wrapt", "fysom") - for package in expected_packages: - self.assertIn( - package, - snapshot["versions"], - f"{package} not found in snapshot['versions']", - ) - self.assertEqual(snapshot["versions"]["instana"], VERSION) diff --git a/tests/propagators/test_base_propagator.py b/tests/propagators/test_base_propagator.py new file mode 100644 index 000000000..5c50e9011 --- /dev/null +++ b/tests/propagators/test_base_propagator.py @@ -0,0 +1,96 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import Generator +from unittest.mock import Mock + +import pytest + +from instana.propagators.base_propagator import BasePropagator + + +class TestBasePropagator: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.propagator = BasePropagator() + yield + self.propagator = None + + def test_extract_headers_dict(self) -> None: + carrier_as_a_dict = {"key": "value"} + assert carrier_as_a_dict == self.propagator.extract_headers_dict( + carrier_as_a_dict + ) + mocked_carrier = Mock() + mocked_carrier.__dict__ = carrier_as_a_dict + assert carrier_as_a_dict == self.propagator.extract_headers_dict(mocked_carrier) + wrong_carrier = "value" + assert self.propagator.extract_headers_dict(wrong_carrier) is None + + def test_get_ctx_level(self) -> None: + assert 3 == self.propagator._get_ctx_level("3,4") + assert 1 == self.propagator._get_ctx_level("wrong_data") + + def test_get_correlation_properties(self) -> None: + a, b = self.propagator._get_correlation_properties( + ",correlationType=3;correlationId=5;" + ) + assert a == "3" + assert b == "5" + assert "3", None == self.propagator._get_correlation_properties( # noqa: E711 + ",correlationType=3;" + ) + + def test_get_participating_trace_context(self, span_context) -> None: + traceparent, tracestate = self.propagator._get_participating_trace_context( + span_context + ) + assert traceparent == "00-00000000000000001926b88ec9ee75ab-5fb1cff576b7e2f5-01" + assert tracestate == "in=1926b88ec9ee75ab;5fb1cff576b7e2f5" + + def test_extract_instana_headers(self) -> None: + dc = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": str.encode(",correlationType=3;correlationId=5;"), + "x-instana-synthetic": "1", + } + trace_id, span_id, level, synthetic = self.propagator.extract_instana_headers( + dc=dc + ) + assert trace_id == "123456789" + assert span_id == "12345" + assert level == ",correlationType=3;correlationId=5;" + assert synthetic + + def test_extract(self) -> None: + carrier = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": str.encode("3,correlationId=5;"), + "x-instana-synthetic": "1", + "traceparent": "00-1812338823475918251-6895521157646639861-01", + "tracestate": "in=1812338823475918251;6895521157646639861", + } + span_context = self.propagator.extract( + carrier=carrier, disable_w3c_trace_context=True + ) + assert span_context + span_context = self.propagator.extract(carrier=carrier) + span_context = self.propagator.extract( + carrier=None, disable_w3c_trace_context=True + ) + assert not span_context + carrier.pop("x-instana-t", None) + carrier.pop("x-instana-s", None) + span_context = self.propagator.extract(carrier=carrier) + assert span_context + carrier = { + "x-instana-t": "123456789", + "x-instana-s": "12345", + "x-instana-l": "2,correlationType=3;correlationId=5;", + "x-instana-synthetic": "1", + "traceparent": "00-4bf92f3577b34da61234567899999999-1234567890888888-01", + "tracestate": "in=1812338823475918251;6895521157646639861", + } + span_context = self.propagator.extract(carrier=carrier) + assert span_context diff --git a/tests/span/test_base_span.py b/tests/span/test_base_span.py index 9a7d8891d..3b2302cf5 100644 --- a/tests/span/test_base_span.py +++ b/tests/span/test_base_span.py @@ -1,7 +1,10 @@ # (c) Copyright IBM Corp. 2024 +from typing import Generator from unittest.mock import Mock, patch +import pytest + from instana.recorder import StanRecorder from instana.span.base_span import BaseSpan from instana.span.span import InstanaSpan @@ -61,7 +64,9 @@ def test_basespan_with_synthetic_source_and_kwargs( assert _kwarg1 == base_span.arg1 -def test_populate_extra_span_attributes(span: InstanaSpan) -> None: +def test_populate_extra_span_attributes( + span: InstanaSpan, +) -> None: base_span = BaseSpan(span, None) base_span._populate_extra_span_attributes(span) @@ -104,7 +109,9 @@ def test_populate_extra_span_attributes_with_values( assert long_id == base_span.crid -def test_validate_attributes(base_span: BaseSpan) -> None: +def test_validate_attributes( + base_span: BaseSpan, +) -> None: attributes = { "field1": 1, "field2": "two", @@ -118,7 +125,9 @@ def test_validate_attributes(base_span: BaseSpan) -> None: assert value in filtered_attributes.values() -def test_validate_attribute_with_invalid_key_type(base_span: BaseSpan) -> None: +def test_validate_attribute_with_invalid_key_type( + base_span: BaseSpan, +) -> None: key = 1 value = "one" @@ -128,7 +137,9 @@ def test_validate_attribute_with_invalid_key_type(base_span: BaseSpan) -> None: assert not validated_value -def test_validate_attribute_exception(span: InstanaSpan) -> None: +def test_validate_attribute_exception( + span: InstanaSpan, +) -> None: base_span = BaseSpan(span, None) key = "field1" value = span @@ -142,7 +153,9 @@ def test_validate_attribute_exception(span: InstanaSpan) -> None: assert not validated_value -def test_convert_attribute_value(span: InstanaSpan) -> None: +def test_convert_attribute_value( + span: InstanaSpan, +) -> None: base_span = BaseSpan(span, None) value = span @@ -150,7 +163,9 @@ def test_convert_attribute_value(span: InstanaSpan) -> None: assert " None: +def test_convert_attribute_value_exception( + base_span: BaseSpan, +) -> None: mock = Mock() mock.__repr__ = Mock(side_effect=Exception("mocked error")) diff --git a/tests/span/test_event.py b/tests/span/test_event.py index baa7521bd..f80e7475c 100644 --- a/tests/span/test_event.py +++ b/tests/span/test_event.py @@ -14,6 +14,7 @@ def test_span_event_defaults(): assert event.name == event_name assert not event.attributes assert isinstance(event.timestamp, int) + assert event.timestamp < time.time_ns() def test_span_event(): @@ -34,3 +35,14 @@ def test_span_event(): assert "field1" in event.attributes.keys() assert "two" == event.attributes.get("field2") assert event.timestamp == timestamp + + +def test_event_with_params() -> None: + name = "sample-event" + attributes = ["attribute"] + timestamp = time.time_ns() + event = Event(name, attributes, timestamp) + + assert event.name == name + assert event.attributes == attributes + assert event.timestamp == timestamp diff --git a/tests/span/test_readable_span.py b/tests/span/test_readable_span.py index 4c4717f22..ca5062272 100644 --- a/tests/span/test_readable_span.py +++ b/tests/span/test_readable_span.py @@ -1,91 +1,86 @@ -import time -from instana.span.readable_span import Event, ReadableSpan -from instana.span_context import SpanContext -from opentelemetry.trace.status import Status, StatusCode - - -def test_event() -> None: - name = "sample-event" - test_event = Event(name) - - assert test_event.name == name - assert not test_event.attributes - assert test_event.timestamp < time.time_ns() +# (c) Copyright IBM Corp. 2024 +import time +from typing import Generator -def test_event_with_params() -> None: - name = "sample-event" - attributes = ["attribute"] - timestamp = time.time_ns() - test_event = Event(name, attributes, timestamp) +import pytest +from opentelemetry.trace.status import Status, StatusCode - assert test_event.name == name - assert test_event.attributes == attributes - assert test_event.timestamp == timestamp +from instana.span.readable_span import Event, ReadableSpan +from instana.span_context import SpanContext -def test_readablespan( - span_context: SpanContext, - trace_id: int, - span_id: int, -) -> None: - span_name = "test-span" - timestamp = time.time_ns() - span = ReadableSpan(span_name, span_context) +class TestReadableSpan: + @pytest.fixture(autouse=True) + def _resource( + self, + ) -> Generator[None, None, None]: + self.span = None + yield - assert span is not None - assert isinstance(span, ReadableSpan) - assert span.name == span_name + def test_readablespan( + self, + span_context: SpanContext, + trace_id: int, + span_id: int, + ) -> None: + span_name = "test-span" + timestamp = time.time_ns() + self.span = ReadableSpan(span_name, span_context) - span_context = span.context - assert isinstance(span_context, SpanContext) - assert span_context.trace_id == trace_id - assert span_context.span_id == span_id + assert self.span is not None + assert isinstance(self.span, ReadableSpan) + assert self.span.name == span_name - assert span.start_time - assert isinstance(span.start_time, int) - assert span.start_time > timestamp - assert not span.end_time - assert not span.attributes - assert not span.events - assert not span.parent_id - assert not span.duration - assert span.status + span_context = self.span.context + assert isinstance(span_context, SpanContext) + assert span_context.trace_id == trace_id + assert span_context.span_id == span_id - assert not span.stack - assert span.synthetic is False + assert self.span.start_time + assert isinstance(self.span.start_time, int) + assert self.span.start_time > timestamp + assert not self.span.end_time + assert not self.span.attributes + assert not self.span.events + assert not self.span.parent_id + assert not self.span.duration + assert self.span.status + assert not self.span.stack + assert self.span.synthetic is False -def test_readablespan_with_params( - span_context: SpanContext, -) -> None: - span_name = "test-span" - parent_id = "123456789" - start_time = time.time_ns() - end_time = time.time_ns() - attributes = {"key": "value"} - event_name = "event" - events = [Event(event_name, attributes, start_time)] - status = Status(StatusCode.OK) - stack = ["span-1", "span-2"] - span = ReadableSpan( - span_name, - span_context, - parent_id, - start_time, - end_time, - attributes, - events, - status, - stack, - ) + def test_readablespan_with_params( + self, + span_context: SpanContext, + ) -> None: + span_name = "test-span" + parent_id = "123456789" + start_time = time.time_ns() + end_time = time.time_ns() + attributes = {"key": "value"} + event_name = "event" + events = [Event(event_name, attributes, start_time)] + status = Status(StatusCode.OK) + stack = ["span-1", "span-2"] + self.span = ReadableSpan( + span_name, + span_context, + parent_id, + start_time, + end_time, + attributes, + events, + status, + stack, + ) - assert span.name == span_name - assert span.parent_id == parent_id - assert span.start_time == start_time - assert span.end_time == end_time - assert span.attributes == attributes - assert span.events == events - assert span.status == status - assert span.duration == end_time - start_time - assert span.stack == stack + assert self.span.name == span_name + assert self.span.parent_id == parent_id + assert self.span.start_time == start_time + assert self.span.end_time == end_time + assert self.span.attributes == attributes + assert self.span.events == events + assert self.span.status == status + assert self.span.duration == end_time - start_time + assert self.span.stack == stack diff --git a/tests/span/test_registered_span.py b/tests/span/test_registered_span.py index f381b1f3e..8d11f7376 100644 --- a/tests/span/test_registered_span.py +++ b/tests/span/test_registered_span.py @@ -1,7 +1,8 @@ # (c) Copyright IBM Corp. 2024 +import logging import time -from typing import Any, Dict, Tuple +from typing import Any, Dict, Generator, Tuple import pytest from opentelemetry.trace import SpanKind @@ -12,422 +13,444 @@ from instana.span_context import SpanContext -@pytest.mark.parametrize( - "span_name, expected_result, attributes", - [ - ("wsgi", ("wsgi", SpanKind.SERVER, "http"), {}), - ("rabbitmq", ("rabbitmq", SpanKind.SERVER, "rabbitmq"), {}), - ("gcps-producer", ("gcps", SpanKind.CLIENT, "gcps"), {}), - ("urllib3", ("urllib3", SpanKind.CLIENT, "http"), {}), - ("rabbitmq", ("rabbitmq", SpanKind.CLIENT, "rabbitmq"), {"sort": "publish"}), - ("render", ("render", SpanKind.INTERNAL, "render"), {"arguments": "--quiet"}), - ], -) -def test_registered_span( - span_context: SpanContext, - span_processor: StanRecorder, - span_name: str, - expected_result: Tuple[str, int, str], - attributes: Dict[str, Any], -) -> None: - service_name = "test-registered-service" - span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) - reg_span = RegisteredSpan(span, None, service_name) - - assert expected_result[0] == reg_span.n - assert expected_result[1] == reg_span.k - assert service_name == reg_span.data["service"] - assert expected_result[2] in reg_span.data.keys() - - -def test_collect_http_attributes_with_attributes( - span_context: SpanContext, span_processor: StanRecorder -) -> None: - span_name = "test-registered-span" - attributes = { - "span.kind": "entry", - "http.host": "localhost", - "http.url": "https://www.instana.com", - "http.header.test": "one more test", - } - service_name = "test-registered-service" - span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) - reg_span = RegisteredSpan(span, None, service_name) - - excepted_result = { - "http.host": attributes["http.host"], - "http.url": attributes["http.url"], - "http.header.test": attributes["http.header.test"], - } - - reg_span._collect_http_attributes(span) - - assert excepted_result["http.host"] == reg_span.data["http"]["host"] - assert excepted_result["http.url"] == reg_span.data["http"]["url"] - assert ( - excepted_result["http.header.test"] == reg_span.data["http"]["header"]["test"] +class TestRegisteredSpan: + @pytest.fixture(autouse=True) + def _resource( + self, + ) -> Generator[None, None, None]: + self.span = None + yield + + @pytest.mark.parametrize( + "span_name, expected_result, attributes", + [ + ("wsgi", ("wsgi", SpanKind.SERVER, "http"), {}), + ("rabbitmq", ("rabbitmq", SpanKind.SERVER, "rabbitmq"), {}), + ("gcps-producer", ("gcps", SpanKind.CLIENT, "gcps"), {}), + ("urllib3", ("urllib3", SpanKind.CLIENT, "http"), {}), + ( + "rabbitmq", + ("rabbitmq", SpanKind.CLIENT, "rabbitmq"), + {"sort": "publish"}, + ), + ( + "render", + ("render", SpanKind.INTERNAL, "render"), + {"arguments": "--quiet"}, + ), + ], ) - - -def test_populate_local_span_data_with_other_name( - span_context: SpanContext, caplog -) -> None: - # span_name = "test-registered-span" - # service_name = "test-registered-service" - # span = InstanaSpan(span_name, span_context) - # reg_span = RegisteredSpan(span, None, service_name) - - # expected_msg = f"SpanRecorder: Unknown local span: {span_name}" - - # reg_span._populate_local_span_data(span) - - # assert expected_msg == caplog.record_tuples[0][2] - pass - - -@pytest.mark.parametrize( - "span_name, service_name, attributes", - [ - ( - "aws.lambda.entry", - "lambda", - { - "lambda.arn": "test", - "lambda.trigger": None, - }, - ), - ( - "celery-worker", - "celery", - { - "host": "localhost", - "port": 1234, - }, - ), - ( - "gcps-consumer", - "gcps", - { - "gcps.op": "consume", - "gcps.projid": "MY_PROJECT", - "gcps.sub": "MY_SUBSCRIPTION_NAME", - }, - ), - ( - "rpc-server", - "rpc", - { - "rpc.flavor": "Vanilla", - "rpc.host": "localhost", - "rpc.port": 1234, - }, - ), - ], -) -def test_populate_entry_span_data( - span_context: SpanContext, - span_processor: StanRecorder, - span_name: str, - service_name: str, - attributes: Dict[str, Any], -) -> None: - span = InstanaSpan(span_name, span_context, span_processor) - reg_span = RegisteredSpan(span, None, service_name) - - expected_result = {} - for attr, value in attributes.items(): - attrl = attr.split(".") - attrl = attrl[1] if len(attrl) > 1 else attrl[0] - expected_result[attrl] = value - - span.set_attributes(attributes) - reg_span._populate_entry_span_data(span) - - for attr, value in expected_result.items(): - assert value == reg_span.data[service_name][attr] - - -@pytest.mark.parametrize( - "attributes", - [ - { - "lambda.arn": "test", - "lambda.trigger": "aws:api.gateway", + def test_registered_span( + self, + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + expected_result: Tuple[str, int, str], + attributes: Dict[str, Any], + ) -> None: + service_name = "test-registered-service" + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes + ) + reg_span = RegisteredSpan(self.span, None, service_name) + + assert expected_result[0] == reg_span.n + assert expected_result[1] == reg_span.k + assert service_name == reg_span.data["service"] + assert expected_result[2] in reg_span.data.keys() + + def test_collect_http_attributes_with_attributes( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-registered-span" + attributes = { + "span.kind": "entry", "http.host": "localhost", "http.url": "https://www.instana.com", - }, - { - "lambda.arn": "test", - "lambda.trigger": "aws:cloudwatch.events", - "lambda.cw.events.resources": "Resource 1", - }, - { - "lambda.arn": "test", - "lambda.trigger": "aws:cloudwatch.logs", - "lambda.cw.logs.group": "My Group", - }, - { - "lambda.arn": "test", - "lambda.trigger": "aws:s3", - "lambda.s3.events": "Event 1", - }, - { - "lambda.arn": "test", - "lambda.trigger": "aws:sqs", - "lambda.sqs.messages": "Message 1", - }, - ], -) -def test_populate_entry_span_data_AWSlambda( - span_context: SpanContext, span_processor: StanRecorder, attributes: Dict[str, Any] -) -> None: - span_name = "aws.lambda.entry" - service_name = "lambda" - expected_result = attributes.copy() - - span = InstanaSpan(span_name, span_context, span_processor) - reg_span = RegisteredSpan(span, None, service_name) - - span.set_attributes(attributes) - reg_span._populate_entry_span_data(span) - - assert "python" == reg_span.data["lambda"]["runtime"] - assert "Unknown" == reg_span.data["lambda"]["functionName"] - assert "test" == reg_span.data["lambda"]["arn"] - assert expected_result["lambda.trigger"] == reg_span.data["lambda"]["trigger"] - - if expected_result["lambda.trigger"] == "aws:api.gateway": - assert expected_result["http.host"] == reg_span.data["http"]["host"] - assert expected_result["http.url"] == reg_span.data["http"]["url"] - - elif expected_result["lambda.trigger"] == "aws:cloudwatch.events": - assert ( - expected_result["lambda.cw.events.resources"] - == reg_span.data["lambda"]["cw"]["events"]["resources"] - ) - elif expected_result["lambda.trigger"] == "aws:cloudwatch.logs": - assert ( - expected_result["lambda.cw.logs.group"] - == reg_span.data["lambda"]["cw"]["logs"]["group"] - ) - elif expected_result["lambda.trigger"] == "aws:s3": - assert ( - expected_result["lambda.s3.events"] - == reg_span.data["lambda"]["s3"]["events"] + "http.header.test": "one more test", + } + service_name = "test-registered-service" + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes ) - elif expected_result["lambda.trigger"] == "aws:sqs": + reg_span = RegisteredSpan(self.span, None, service_name) + + excepted_result = { + "http.host": attributes["http.host"], + "http.url": attributes["http.url"], + "http.header.test": attributes["http.header.test"], + } + + reg_span._collect_http_attributes(self.span) + + assert excepted_result["http.host"] == reg_span.data["http"]["host"] + assert excepted_result["http.url"] == reg_span.data["http"]["url"] assert ( - expected_result["lambda.sqs.messages"] - == reg_span.data["lambda"]["sqs"]["messages"] + excepted_result["http.header.test"] + == reg_span.data["http"]["header"]["test"] ) - -@pytest.mark.parametrize( - "span_name, service_name, attributes", - [ - ( - "cassandra", - "cassandra", - { - "cassandra.cluster": "my_cluster", - "cassandra.error": "minor error", - }, - ), - ( - "celery-client", - "celery", - { - "host": "localhost", - "port": 1234, - }, - ), - ( - "couchbase", - "couchbase", - { - "couchbase.hostname": "localhost", - "couchbase.error_type": 1234, - }, - ), - ( - "rabbitmq", - "rabbitmq", - { - "address": "localhost", - "key": 1234, - }, - ), - ( - "redis", - "redis", - { - "command": "ls -l", - "redis.error": "minor error", - }, - ), - ( - "rpc-client", - "rpc", - { - "rpc.flavor": "Vanilla", - "rpc.host": "localhost", - "rpc.port": 1234, - }, - ), - ( - "sqlalchemy", - "sqlalchemy", - { - "sqlalchemy.sql": "SELECT * FROM everything;", - "sqlalchemy.err": "Impossible select everything from everything!", - }, - ), - ( - "mysql", - "mysql", + def test_populate_local_span_data_with_other_name( + self, + span_context: SpanContext, + span_processor, + caplog, + ) -> None: + caplog.set_level(logging.DEBUG, logger="instana") + span_name = "test-registered-span" + service_name = "test-registered-service" + self.span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(self.span, None, service_name) + + expected_msg = f"SpanRecorder: Unknown local span: {span_name}" + + reg_span._populate_local_span_data(self.span) + + assert expected_msg in caplog.messages + + @pytest.mark.parametrize( + "span_name, service_name, attributes", + [ + ( + "aws.lambda.entry", + "lambda", + { + "lambda.arn": "test", + "lambda.trigger": None, + }, + ), + ( + "celery-worker", + "celery", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "gcps-consumer", + "gcps", + { + "gcps.op": "consume", + "gcps.projid": "MY_PROJECT", + "gcps.sub": "MY_SUBSCRIPTION_NAME", + }, + ), + ( + "rpc-server", + "rpc", + { + "rpc.flavor": "Vanilla", + "rpc.host": "localhost", + "rpc.port": 1234, + }, + ), + ], + ) + def test_populate_entry_span_data( + self, + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + service_name: str, + attributes: Dict[str, Any], + ) -> None: + self.span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(self.span, None, service_name) + + expected_result = {} + for attr, value in attributes.items(): + attrl = attr.split(".") + attrl = attrl[1] if len(attrl) > 1 else attrl[0] + expected_result[attrl] = value + + self.span.set_attributes(attributes) + reg_span._populate_entry_span_data(self.span) + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + @pytest.mark.parametrize( + "attributes", + [ { - "host": "localhost", - "port": 1234, + "lambda.arn": "test", + "lambda.trigger": "aws:api.gateway", + "http.host": "localhost", + "http.url": "https://www.instana.com", }, - ), - ( - "postgres", - "pg", { - "host": "localhost", - "port": 1234, + "lambda.arn": "test", + "lambda.trigger": "aws:cloudwatch.events", + "lambda.cw.events.resources": "Resource 1", }, - ), - ( - "mongo", - "mongo", { - "command": "IDK", - "error": "minor error", + "lambda.arn": "test", + "lambda.trigger": "aws:cloudwatch.logs", + "lambda.cw.logs.group": "My Group", }, - ), - ( - "gcs", - "gcs", { - "gcs.op": "produce", - "gcs.projectId": "MY_PROJECT", - "gcs.accessId": "Can not tell you!", + "lambda.arn": "test", + "lambda.trigger": "aws:s3", + "lambda.s3.events": "Event 1", }, - ), - ( - "gcps-producer", - "gcps", { - "gcps.op": "produce", - "gcps.projid": "MY_PROJECT", - "gcps.top": "MY_SUBSCRIPTION_NAME", - }, - ), - ], -) -def test_populate_exit_span_data( - span_context: SpanContext, - span_processor: StanRecorder, - span_name: str, - service_name: str, - attributes: Dict[str, Any], -) -> None: - span = InstanaSpan(span_name, span_context, span_processor) - reg_span = RegisteredSpan(span, None, service_name) - - expected_result = {} - for attr, value in attributes.items(): - attrl = attr.split(".") - attrl = attrl[1] if len(attrl) > 1 else attrl[0] - expected_result[attrl] = value - - span.set_attributes(attributes) - reg_span._populate_exit_span_data(span) - - for attr, value in expected_result.items(): - assert value == reg_span.data[service_name][attr] - - -@pytest.mark.parametrize( - "attributes", - [ - { - "op": "test", - "http.host": "localhost", - "http.url": "https://www.instana.com", - }, - { - "payload": { - "blah": "bleh", - "blih": "bloh", + "lambda.arn": "test", + "lambda.trigger": "aws:sqs", + "lambda.sqs.messages": "Message 1", }, - "http.host": "localhost", - "http.url": "https://www.instana.com", - }, - ], -) -def test_populate_exit_span_data_boto3( - span_context: SpanContext, span_processor: StanRecorder, attributes: Dict[str, Any] -) -> None: - span_name = service_name = "boto3" - expected_result = attributes.copy() - - span = InstanaSpan(span_name, span_context, span_processor) - reg_span = RegisteredSpan(span, None, service_name) - - # expected_result = {} - # for attr, value in attributes.items(): - # attrl = attr.split(".") - # attrl = attrl[1] if len(attrl) > 1 else attrl[0] - # expected_result[attrl] = value - - span.set_attributes(attributes) - reg_span._populate_exit_span_data(span) - - assert expected_result.pop("http.host", None) == reg_span.data["http"]["host"] - assert expected_result.pop("http.url", None) == reg_span.data["http"]["url"] - - for attr, value in expected_result.items(): - assert value == reg_span.data[service_name][attr] - - -def test_populate_exit_span_data_log( - span_context: SpanContext, span_processor: StanRecorder -) -> None: - span_name = service_name = "log" - sample_span = InstanaSpan(span_name, span_context, span_processor) - reg_span = RegisteredSpan(sample_span, None, service_name) - - excepted_text = "Houston, we have a problem!" - sample_events = [ - ( - "test_populate_exit_span_data_log_event_with_message", + ], + ) + def test_populate_entry_span_data_AWSlambda( + self, + span_context: SpanContext, + span_processor: StanRecorder, + attributes: Dict[str, Any], + ) -> None: + span_name = "aws.lambda.entry" + service_name = "lambda" + expected_result = attributes.copy() + + self.span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(self.span, None, service_name) + + self.span.set_attributes(attributes) + reg_span._populate_entry_span_data(self.span) + + assert "python" == reg_span.data["lambda"]["runtime"] + assert "Unknown" == reg_span.data["lambda"]["functionName"] + assert "test" == reg_span.data["lambda"]["arn"] + assert expected_result["lambda.trigger"] == reg_span.data["lambda"]["trigger"] + + if expected_result["lambda.trigger"] == "aws:api.gateway": + assert expected_result["http.host"] == reg_span.data["http"]["host"] + assert expected_result["http.url"] == reg_span.data["http"]["url"] + + elif expected_result["lambda.trigger"] == "aws:cloudwatch.events": + assert ( + expected_result["lambda.cw.events.resources"] + == reg_span.data["lambda"]["cw"]["events"]["resources"] + ) + elif expected_result["lambda.trigger"] == "aws:cloudwatch.logs": + assert ( + expected_result["lambda.cw.logs.group"] + == reg_span.data["lambda"]["cw"]["logs"]["group"] + ) + elif expected_result["lambda.trigger"] == "aws:s3": + assert ( + expected_result["lambda.s3.events"] + == reg_span.data["lambda"]["s3"]["events"] + ) + elif expected_result["lambda.trigger"] == "aws:sqs": + assert ( + expected_result["lambda.sqs.messages"] + == reg_span.data["lambda"]["sqs"]["messages"] + ) + + @pytest.mark.parametrize( + "span_name, service_name, attributes", + [ + ( + "cassandra", + "cassandra", + { + "cassandra.cluster": "my_cluster", + "cassandra.error": "minor error", + }, + ), + ( + "celery-client", + "celery", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "couchbase", + "couchbase", + { + "couchbase.hostname": "localhost", + "couchbase.error_type": 1234, + }, + ), + ( + "rabbitmq", + "rabbitmq", + { + "address": "localhost", + "key": 1234, + }, + ), + ( + "redis", + "redis", + { + "command": "ls -l", + "redis.error": "minor error", + }, + ), + ( + "rpc-client", + "rpc", + { + "rpc.flavor": "Vanilla", + "rpc.host": "localhost", + "rpc.port": 1234, + }, + ), + ( + "sqlalchemy", + "sqlalchemy", + { + "sqlalchemy.sql": "SELECT * FROM everything;", + "sqlalchemy.err": "Impossible select everything from everything!", + }, + ), + ( + "mysql", + "mysql", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "postgres", + "pg", + { + "host": "localhost", + "port": 1234, + }, + ), + ( + "mongo", + "mongo", + { + "command": "IDK", + "error": "minor error", + }, + ), + ( + "gcs", + "gcs", + { + "gcs.op": "produce", + "gcs.projectId": "MY_PROJECT", + "gcs.accessId": "Can not tell you!", + }, + ), + ( + "gcps-producer", + "gcps", + { + "gcps.op": "produce", + "gcps.projid": "MY_PROJECT", + "gcps.top": "MY_SUBSCRIPTION_NAME", + }, + ), + ], + ) + def test_populate_exit_span_data( + self, + span_context: SpanContext, + span_processor: StanRecorder, + span_name: str, + service_name: str, + attributes: Dict[str, Any], + ) -> None: + self.span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(self.span, None, service_name) + + expected_result = {} + for attr, value in attributes.items(): + attrl = attr.split(".") + attrl = attrl[1] if len(attrl) > 1 else attrl[0] + expected_result[attrl] = value + + self.span.set_attributes(attributes) + reg_span._populate_exit_span_data(self.span) + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + @pytest.mark.parametrize( + "attributes", + [ { - "field1": 1, - "field2": "two", - "message": excepted_text, + "op": "test", + "http.host": "localhost", + "http.url": "https://www.instana.com", }, - time.time_ns(), - ), - ( - "test_populate_exit_span_data_log_event_with_parameters", { - "field1": 1, - "field2": "two", - "parameters": excepted_text, + "payload": { + "blah": "bleh", + "blih": "bloh", + }, + "http.host": "localhost", + "http.url": "https://www.instana.com", }, - time.time_ns(), - ), - ] - - for event_name, attributes, timestamp in sample_events: - sample_span.add_event(event_name, attributes, timestamp) - - reg_span._populate_exit_span_data(sample_span) - - assert excepted_text == reg_span.data["log"]["message"] - assert excepted_text == reg_span.data["log"]["parameters"] - - while sample_span._events: - sample_span._events.pop() + ], + ) + def test_populate_exit_span_data_boto3( + self, + span_context: SpanContext, + span_processor: StanRecorder, + attributes: Dict[str, Any], + ) -> None: + span_name = service_name = "boto3" + expected_result = attributes.copy() + + self.span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(self.span, None, service_name) + + self.span.set_attributes(attributes) + reg_span._populate_exit_span_data(self.span) + + assert expected_result.pop("http.host", None) == reg_span.data["http"]["host"] + assert expected_result.pop("http.url", None) == reg_span.data["http"]["url"] + + for attr, value in expected_result.items(): + assert value == reg_span.data[service_name][attr] + + def test_populate_exit_span_data_log( + self, span_context: SpanContext, span_processor: StanRecorder + ) -> None: + span_name = service_name = "log" + self.span = InstanaSpan(span_name, span_context, span_processor) + reg_span = RegisteredSpan(self.span, None, service_name) + + excepted_text = "Houston, we have a problem!" + sample_events = [ + ( + "test_populate_exit_span_data_log_event_with_message", + { + "field1": 1, + "field2": "two", + "message": excepted_text, + }, + time.time_ns(), + ), + ( + "test_populate_exit_span_data_log_event_with_parameters", + { + "field1": 1, + "field2": "two", + "parameters": excepted_text, + }, + time.time_ns(), + ), + ] + + for event_name, attributes, timestamp in sample_events: + self.span.add_event(event_name, attributes, timestamp) + + reg_span._populate_exit_span_data(self.span) + + assert excepted_text == reg_span.data["log"]["message"] + assert excepted_text == reg_span.data["log"]["parameters"] + + while self.span._events: + self.span._events.pop() diff --git a/tests/span/test_span_sdk.py b/tests/span/test_span_sdk.py index 8ee9f9e2c..5256f7fb8 100644 --- a/tests/span/test_span_sdk.py +++ b/tests/span/test_span_sdk.py @@ -1,6 +1,6 @@ # (c) Copyright IBM Corp. 2024 -from typing import Tuple +from typing import Generator, Tuple import pytest @@ -10,79 +10,93 @@ from instana.span_context import SpanContext -def test_sdkspan(span_context: SpanContext, span_processor: StanRecorder) -> None: - span_name = "test-sdk-span" - service_name = "test-sdk" - attributes = { - "span.kind": "entry", - "arguments": "--quiet", - "return": "True", - } - span = InstanaSpan(span_name, span_context, span_processor, attributes=attributes) - sdk_span = SDKSpan(span, None, service_name) +class TestSDKSpan: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.span = None + yield - expected_result = { - "n": "sdk", - "k": 1, - "data": { - "service": service_name, - "sdk": { - "name": span_name, - "type": attributes["span.kind"], - "custom": { - "attributes": attributes, + def test_sdkspan( + self, + span_context: SpanContext, + span_processor: StanRecorder, + ) -> None: + span_name = "test-sdk-span" + service_name = "test-sdk" + attributes = { + "span.kind": "entry", + "arguments": "--quiet", + "return": "True", + } + self.span = InstanaSpan( + span_name, span_context, span_processor, attributes=attributes + ) + sdk_span = SDKSpan(self.span, None, service_name) + + expected_result = { + "n": "sdk", + "k": 1, + "data": { + "service": service_name, + "sdk": { + "name": span_name, + "type": attributes["span.kind"], + "custom": { + "attributes": attributes, + }, + "arguments": attributes["arguments"], + "return": attributes["return"], }, - "arguments": attributes["arguments"], - "return": attributes["return"], }, - }, - } - - assert expected_result["n"] == sdk_span.n - assert expected_result["k"] == sdk_span.k - assert len(expected_result["data"]) == len(sdk_span.data) - assert expected_result["data"]["service"] == sdk_span.data["service"] - assert len(expected_result["data"]["sdk"]) == len(sdk_span.data["sdk"]) - assert expected_result["data"]["sdk"]["name"] == sdk_span.data["sdk"]["name"] - assert expected_result["data"]["sdk"]["type"] == sdk_span.data["sdk"]["type"] - assert len(attributes) == len(sdk_span.data["sdk"]["custom"]["tags"]) - assert attributes == sdk_span.data["sdk"]["custom"]["tags"] - assert attributes["arguments"] == sdk_span.data["sdk"]["arguments"] - assert attributes["return"] == sdk_span.data["sdk"]["return"] + } + assert expected_result["n"] == sdk_span.n + assert expected_result["k"] == sdk_span.k + assert len(expected_result["data"]) == len(sdk_span.data) + assert expected_result["data"]["service"] == sdk_span.data["service"] + assert len(expected_result["data"]["sdk"]) == len(sdk_span.data["sdk"]) + assert expected_result["data"]["sdk"]["name"] == sdk_span.data["sdk"]["name"] + assert expected_result["data"]["sdk"]["type"] == sdk_span.data["sdk"]["type"] + assert len(attributes) == len(sdk_span.data["sdk"]["custom"]["tags"]) + assert attributes == sdk_span.data["sdk"]["custom"]["tags"] + assert attributes["arguments"] == sdk_span.data["sdk"]["arguments"] + assert attributes["return"] == sdk_span.data["sdk"]["return"] -@pytest.mark.parametrize( - "span_kind, expected_result", - [ - (None, ("intermediate", 3)), - ("entry", ("entry", 1)), - ("server", ("entry", 1)), - ("consumer", ("entry", 1)), - ("exit", ("exit", 2)), - ("client", ("exit", 2)), - ("producer", ("exit", 2)), - ], -) -def test_sdkspan_get_span_kind( - span_context: SpanContext, - span_processor: StanRecorder, - span_kind: str, - expected_result: Tuple[str, int], -) -> None: - attributes = { - "span.kind": span_kind, - } - span = InstanaSpan( - "test-sdk-span", span_context, span_processor, attributes=attributes + @pytest.mark.parametrize( + "span_kind, expected_result", + [ + (None, ("intermediate", 3)), + ("entry", ("entry", 1)), + ("server", ("entry", 1)), + ("consumer", ("entry", 1)), + ("exit", ("exit", 2)), + ("client", ("exit", 2)), + ("producer", ("exit", 2)), + ], ) - sdk_span = SDKSpan(span, None, "test") - - kind = sdk_span.get_span_kind(span) + def test_sdkspan_get_span_kind( + self, + span_context: SpanContext, + span_processor: StanRecorder, + span_kind: str, + expected_result: Tuple[str, int], + ) -> None: + attributes = { + "span.kind": span_kind, + } + self.span = InstanaSpan( + "test-sdk-span", span_context, span_processor, attributes=attributes + ) + sdk_span = SDKSpan(self.span, None, "test") - assert expected_result == kind + kind = sdk_span.get_span_kind(self.span) + assert expected_result == kind -def test_sdkspan_get_span_kind_with_no_attributes(span: InstanaSpan) -> None: - sdk_span = SDKSpan(span, None, "test") - kind = sdk_span.get_span_kind(span) - assert ("intermediate", 3) == kind + def test_sdkspan_get_span_kind_with_no_attributes( + self, + span: InstanaSpan, + ) -> None: + self.span = SDKSpan(span, None, "test") + kind = self.span.get_span_kind(span) + assert ("intermediate", 3) == kind diff --git a/tests/test_sampling.py b/tests/test_sampling.py new file mode 100644 index 000000000..3ab6eaaa4 --- /dev/null +++ b/tests/test_sampling.py @@ -0,0 +1,23 @@ +# (c) Copyright IBM Corp. 2024 + +from typing import Generator + +import pytest + +from instana.sampling import InstanaSampler, SamplingPolicy + + +class TestInstanaSampler: + @pytest.fixture(autouse=True) + def _resource(self) -> Generator[None, None, None]: + self.sampler = InstanaSampler() + yield + self.sampler = None + + def test_sampling_policy(self) -> None: + assert self.sampler._sampled == SamplingPolicy.DROP + assert self.sampler._sampled.name == "DROP" + assert self.sampler._sampled.value == 0 + + def test_sampler(self) -> None: + assert not self.sampler.sampled() diff --git a/tests/util/test_traceutils.py b/tests/util/test_traceutils.py new file mode 100644 index 000000000..a5505c1f2 --- /dev/null +++ b/tests/util/test_traceutils.py @@ -0,0 +1,62 @@ +# (c) Copyright IBM Corp. 2024 + +from unittest.mock import patch + +from instana.singletons import agent, tracer +from instana.tracer import InstanaTracer +from instana.util.traceutils import ( + extract_custom_headers, + get_active_tracer, + get_tracer_tuple, + tracing_is_off, +) + + +def test_extract_custom_headers(span) -> None: + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + request_headers = { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + } + extract_custom_headers(span, request_headers) + assert len(span.attributes) == 2 + assert span.attributes["http.header.X-Capture-This-Too"] == "this too" + assert span.attributes["http.header.X-Capture-That-Too"] == "that too" + + +def test_get_activate_tracer() -> None: + assert not get_active_tracer() + + with tracer.start_as_current_span("test"): + response = get_active_tracer() + assert isinstance(response, InstanaTracer) + assert response == tracer + with patch("instana.span.span.InstanaSpan.is_recording", return_value=False): + assert not get_active_tracer() + + +def test_get_tracer_tuple() -> None: + response = get_tracer_tuple() + assert response == (None, None, None) + + agent.options.allow_exit_as_root = True + response = get_tracer_tuple() + assert response == (tracer, None, None) + agent.options.allow_exit_as_root = False + + with tracer.start_as_current_span("test") as span: + response = get_tracer_tuple() + assert response == (tracer, span, span.name) + + +def test_tracing_is_off() -> None: + response = tracing_is_off() + assert response + with tracer.start_as_current_span("test"): + response = tracing_is_off() + assert not response + + agent.options.allow_exit_as_root = True + response = tracing_is_off() + assert not response + agent.options.allow_exit_as_root = False