diff --git a/src/instana/agent/host.py b/src/instana/agent/host.py index 282bbf53..72689059 100644 --- a/src/instana/agent/host.py +++ b/src/instana/agent/host.py @@ -315,7 +315,7 @@ def report_data_payload( def report_metrics(self, payload: Dict[str, Any]) -> Optional[Response]: metrics = payload.get("metrics", []) - if len(metrics) > 0: + if len(metrics) > 0 and len(metrics.get("plugins", [])) > 0: metric_bundle = metrics["plugins"][0]["data"] response = self.client.post( self.__data_url(), diff --git a/src/instana/collector/base.py b/src/instana/collector/base.py index de255c34..3a8e8db8 100644 --- a/src/instana/collector/base.py +++ b/src/instana/collector/base.py @@ -49,6 +49,9 @@ def __init__(self, agent: Type["BaseAgent"]) -> None: # How often to report snapshot data (in seconds) self.snapshot_data_interval = 300 + # Timestamp in seconds of the last time we sent metrics data + self.metrics_data_last_sent = 0 + # List of helpers that help out in data collection self.helpers = [] @@ -58,6 +61,7 @@ def __init__(self, agent: Type["BaseAgent"]) -> None: self.background_report_lock = threading.RLock() # Reporting interval for the background thread(s) + # Default is 1 but can be changed by the agent options self.report_interval = 1 # Flag to indicate if start/shutdown state diff --git a/src/instana/collector/host.py b/src/instana/collector/host.py index b3df855e..5a5e7f44 100644 --- a/src/instana/collector/host.py +++ b/src/instana/collector/host.py @@ -72,6 +72,17 @@ def should_send_snapshot_data(self) -> bool: delta = int(time()) - self.snapshot_data_last_sent return delta > self.snapshot_data_interval + def should_send_metrics(self) -> bool: + """ + Determines if metrics data should be sent based on poll_rate. + """ + poll_rate = 1 + if hasattr(self.agent, "options") and hasattr(self.agent.options, "poll_rate"): + poll_rate = self.agent.options.poll_rate + + delta = int(time()) - self.metrics_data_last_sent + return delta >= poll_rate + def prepare_payload(self) -> DefaultDict[Any, Any]: payload = DictionaryOfStan() payload["spans"] = [] @@ -79,22 +90,28 @@ def prepare_payload(self) -> DefaultDict[Any, Any]: payload["metrics"]["plugins"] = [] try: + # Always collect and send spans immediately (every 1 second) if not self.span_queue.empty(): payload["spans"] = format_span(self.queued_spans()) if not self.profile_queue.empty(): payload["profiles"] = self.queued_profiles() - with_snapshot = self.should_send_snapshot_data() + # Only collect metrics based on poll_rate interval + if self.should_send_metrics(): + with_snapshot = self.should_send_snapshot_data() + + plugins = [] + for helper in self.helpers: + plugins.extend(helper.collect_metrics(with_snapshot=with_snapshot)) - plugins = [] - for helper in self.helpers: - plugins.extend(helper.collect_metrics(with_snapshot=with_snapshot)) + payload["metrics"]["plugins"] = plugins - payload["metrics"]["plugins"] = plugins + if with_snapshot is True: + self.snapshot_data_last_sent = int(time()) - if with_snapshot is True: - self.snapshot_data_last_sent = int(time()) + # Update metrics last sent timestamp + self.metrics_data_last_sent = int(time()) except Exception: logger.debug("non-fatal prepare_payload:", exc_info=True) diff --git a/src/instana/options.py b/src/instana/options.py index 372bbfbd..f63ed9c7 100644 --- a/src/instana/options.py +++ b/src/instana/options.py @@ -351,12 +351,15 @@ class StandardOptions(BaseOptions): AGENT_DEFAULT_HOST = "localhost" AGENT_DEFAULT_PORT = 42699 + DEFAULT_POLL_RATE = 1 + MAX_POLL_RATE = 5 def __init__(self, **kwds: Dict[str, Any]) -> None: super(StandardOptions, self).__init__() self.agent_host = os.environ.get("INSTANA_AGENT_HOST", self.AGENT_DEFAULT_HOST) self.agent_port = os.environ.get("INSTANA_AGENT_PORT", self.AGENT_DEFAULT_PORT) + self.poll_rate = self.DEFAULT_POLL_RATE if not isinstance(self.agent_port, int): self.agent_port = int(self.agent_port) @@ -506,6 +509,34 @@ def set_disable_tracing(self, tracing_config: Sequence[Dict[str, Any]]) -> None: self.disabled_spans.extend(disabled_spans) self.enabled_spans.extend(enabled_spans) + def set_poll_rate(self, plugin_config: Dict[str, Any]) -> None: + """Set poll rate from agent plugin configuration.""" + poll_rate_value = plugin_config.get("poll_rate") + if poll_rate_value is None: + return + + try: + poll_rate = int(poll_rate_value) + except (ValueError, TypeError): + logger.debug( + f"Invalid poll_rate type, defaulting to {self.DEFAULT_POLL_RATE}" + ) + self.poll_rate = self.DEFAULT_POLL_RATE + return + + if poll_rate in (self.DEFAULT_POLL_RATE, self.MAX_POLL_RATE): + self.poll_rate = poll_rate + logger.debug( + f"Poll rate set to {self.poll_rate} seconds from agent configuration" + ) + return + + logger.debug( + f"Invalid poll_rate value {poll_rate}, defaulting to " + f"{self.DEFAULT_POLL_RATE}" + ) + self.poll_rate = self.DEFAULT_POLL_RATE + def set_from(self, res_data: Dict[str, Any]) -> None: """ Set the source identifiers given to use by the Instana Host agent. @@ -516,13 +547,19 @@ def set_from(self, res_data: Dict[str, Any]) -> None: logger.debug(f"options.set_from: Wrong data type - {type(res_data)}") return + # Extract poll_rate from plugin.python.poll_rate + if "plugin" in res_data and isinstance(res_data["plugin"], dict): + python_plugin = res_data["plugin"].get("python") + if isinstance(python_plugin, dict): + self.set_poll_rate(python_plugin) + if "secrets" in res_data: self.set_secrets(res_data["secrets"]) if "tracing" in res_data: self.set_tracing(res_data["tracing"]) - else: + # Rely on extra headers if no tracing configuration comes from the agent if "extraHeaders" in res_data: self.set_extra_headers(res_data["extraHeaders"]) diff --git a/tests/agent/test_host.py b/tests/agent/test_host.py index 7cd1da3e..29596b6f 100644 --- a/tests/agent/test_host.py +++ b/tests/agent/test_host.py @@ -286,9 +286,10 @@ def test_agent_connection_attempt_fails_with_404( reason='Avoiding "psutil.NoSuchProcess: process PID not found (pid=12345)"', ) def test_init(self) -> None: - with patch( - "instana.agent.base.BaseAgent.update_log_level" - ) as mock_update, patch.object(os, "getpid", return_value=12345): + 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 @@ -320,9 +321,10 @@ def test_handle_fork( def test_reset( self, ) -> None: - with patch( - "instana.collector.host.HostCollector.shutdown" - ) as mock_shutdown, patch("instana.fsm.TheMachine.reset") as mock_reset: + with ( + patch("instana.collector.host.HostCollector.shutdown") as mock_shutdown, + patch("instana.fsm.TheMachine.reset") as mock_reset, + ): agent = HostAgent() agent.reset() @@ -356,9 +358,11 @@ def test_can_send( ) -> 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): + 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() @@ -438,9 +442,11 @@ def test_announce( agent = HostAgent() mock_response = Mock() mock_response.status_code = 200 - mock_response.content = json.dumps( - {"get": "value", "pid": "value", "agentUuid": "value"} - ) + 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 @@ -449,9 +455,11 @@ def test_announce( 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.content = json.dumps({ + "get": "value", + "pid": "value", + "agentUuid": "value", + }) with patch.object(requests.Session, "put", side_effect=Exception()): caplog.set_level(logging.DEBUG, logger="instana") @@ -506,9 +514,10 @@ def test_log_message_to_host_agent( 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: + 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") @@ -532,9 +541,12 @@ def test_is_agent_ready( 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", + 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()): @@ -567,15 +579,20 @@ def test_report_data_payload( 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", + 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) @@ -596,19 +613,88 @@ def test_report_metrics(self) -> None: }, } - 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", + 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_metrics(payload) assert test_response.return_value == "Success" + def test_report_metrics_with_empty_plugins(self) -> None: + """Test that report_metrics returns None when plugins list is empty""" + agent = HostAgent() + + # Payload with empty plugins list + payload = { + "metrics": {"plugins": []}, + } + + # Should return None without making any HTTP request + result = agent.report_metrics(payload) + assert result is None + + def test_report_metrics_with_no_plugins_key(self) -> None: + """Test that report_metrics returns None when plugins key is missing""" + agent = HostAgent() + + # Payload without plugins key + payload = {"metrics": {}} + + # Should return None without making any HTTP request + result = agent.report_metrics(payload) + assert result is None + + def test_report_metrics_with_valid_plugins(self) -> None: + """Test that report_metrics works correctly with valid plugins""" + agent = HostAgent() + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.return_value = "Success" + + payload = { + "metrics": { + "plugins": [ + { + "data": { + "cpu_usage": 45.5, + "memory_usage": 1024, + } + }, + ] + }, + } + + with ( + patch.object( + requests.Session, "post", return_value=mock_response + ) as mock_post, + patch( + "instana.agent.host.HostAgent._HostAgent__data_url", + return_value="http://localhost:42699/metrics", + ), + ): + result = agent.report_metrics(payload) + + # Verify the request was made + assert mock_post.called + assert result == mock_response + + # Verify the correct data was sent + call_args = mock_post.call_args + assert call_args is not None + def test_report_profiles(self) -> None: agent = HostAgent() @@ -620,15 +706,20 @@ def test_report_profiles(self) -> None: "profiles": ["profile-1", "profile-2"], } - 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", + 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_profiles(payload) assert test_response.return_value == "Success" @@ -652,15 +743,20 @@ def test_report_spans( "spans": [span_1, span_2], } - 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", + 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_spans(payload) assert test_response.return_value == "Success" @@ -725,26 +821,31 @@ def test_is_service_or_endpoint_ignored(self) -> None: # ignore all endpoints of service1 assert self.agent._HostAgent__is_endpoint_ignored({"type": "service1"}) - assert self.agent._HostAgent__is_endpoint_ignored( - {"type": "service1", "endpoint": "method1"} - ) - assert self.agent._HostAgent__is_endpoint_ignored( - {"type": "service1", "endpoint": "method2"} - ) + assert self.agent._HostAgent__is_endpoint_ignored({ + "type": "service1", + "endpoint": "method1", + }) + assert self.agent._HostAgent__is_endpoint_ignored({ + "type": "service1", + "endpoint": "method2", + }) # ignore only endpoint1 of service2 - assert self.agent._HostAgent__is_endpoint_ignored( - {"type": "service2", "endpoint": "method1"} - ) - assert not self.agent._HostAgent__is_endpoint_ignored( - {"type": "service2", "endpoint": "method2"} - ) + assert self.agent._HostAgent__is_endpoint_ignored({ + "type": "service2", + "endpoint": "method1", + }) + assert not self.agent._HostAgent__is_endpoint_ignored({ + "type": "service2", + "endpoint": "method2", + }) # don't ignore other services assert not self.agent._HostAgent__is_endpoint_ignored({"type": "service3"}) - assert not self.agent._HostAgent__is_endpoint_ignored( - {"type": "service3", "endpoint": "method1"} - ) + assert not self.agent._HostAgent__is_endpoint_ignored({ + "type": "service3", + "endpoint": "method1", + }) @pytest.mark.parametrize( "input_data", diff --git a/tests/collector/test_host_collector.py b/tests/collector/test_host_collector.py index 2a9d68e4..1d950328 100644 --- a/tests/collector/test_host_collector.py +++ b/tests/collector/test_host_collector.py @@ -87,6 +87,156 @@ 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_should_send_metrics_with_default_poll_rate(self) -> None: + """Test that metrics should be sent immediately with default poll_rate of 1 second""" + # Initially, metrics_data_last_sent is 0, so should return True + assert self.agent.collector.should_send_metrics() + + # After updating timestamp, should return False immediately + from time import time + + self.agent.collector.metrics_data_last_sent = int(time()) + assert not self.agent.collector.should_send_metrics() + + def test_should_send_metrics_with_custom_poll_rate(self) -> None: + """Test that metrics respect custom poll_rate from agent options""" + from time import time + from instana.options import StandardOptions + + # Set custom poll_rate of 5 seconds + self.agent.options = StandardOptions() + self.agent.options.poll_rate = 5 + + # Initially should return True + assert self.agent.collector.should_send_metrics() + + # Set timestamp to now + current_time = int(time()) + self.agent.collector.metrics_data_last_sent = current_time + + # Should return False immediately after + assert not self.agent.collector.should_send_metrics() + + # Simulate 3 seconds passing (less than poll_rate) + self.agent.collector.metrics_data_last_sent = current_time - 3 + assert not self.agent.collector.should_send_metrics() + + # Simulate 5 seconds passing (equal to poll_rate) + self.agent.collector.metrics_data_last_sent = current_time - 5 + assert self.agent.collector.should_send_metrics() + + # Simulate 6 seconds passing (more than poll_rate) + self.agent.collector.metrics_data_last_sent = current_time - 6 + assert self.agent.collector.should_send_metrics() + + def test_should_send_metrics_without_agent_options(self) -> None: + """Test that should_send_metrics works when agent has no options attribute""" + from time import time + + # Remove options attribute to test fallback + if hasattr(self.agent, "options"): + delattr(self.agent, "options") + + # Should use default poll_rate of 1 + assert self.agent.collector.should_send_metrics() + + self.agent.collector.metrics_data_last_sent = int(time()) + assert not self.agent.collector.should_send_metrics() + + def test_prepare_payload_respects_poll_rate(self) -> None: + """Test that prepare_payload only collects metrics based on poll_rate""" + from time import time + from instana.options import StandardOptions + + # Set poll_rate to 5 seconds + self.agent.options = StandardOptions() + self.agent.options.poll_rate = 5 + + with patch.object(gc, "isenabled", return_value=True): + # First call should collect metrics + self.agent.collector.metrics_data_last_sent = 0 + payload = self.agent.collector.prepare_payload() + assert payload + assert "metrics" in payload + assert "plugins" in payload["metrics"] + assert len(payload["metrics"]["plugins"]) == 1 + + # Immediately after, should not collect metrics (empty plugins) + payload = self.agent.collector.prepare_payload() + assert payload + assert "metrics" in payload + assert "plugins" in payload["metrics"] + assert len(payload["metrics"]["plugins"]) == 0 + + # Simulate 5 seconds passing + self.agent.collector.metrics_data_last_sent = int(time()) - 5 + payload = self.agent.collector.prepare_payload() + assert payload + assert "metrics" in payload + assert "plugins" in payload["metrics"] + assert len(payload["metrics"]["plugins"]) == 1 + + def test_metrics_data_last_sent_updated(self) -> None: + """Test that metrics_data_last_sent timestamp is updated after collecting metrics""" + from time import time + from instana.options import StandardOptions + + self.agent.options = StandardOptions() + self.agent.options.poll_rate = 1 + + with patch.object(gc, "isenabled", return_value=True): + # Reset timestamp + self.agent.collector.metrics_data_last_sent = 0 + initial_time = int(time()) + + # Prepare payload should update timestamp + payload = self.agent.collector.prepare_payload() + assert payload + + # Verify timestamp was updated + assert self.agent.collector.metrics_data_last_sent >= initial_time + assert self.agent.collector.metrics_data_last_sent <= int(time()) + + def test_prepare_payload_spans_always_collected(self) -> None: + """Test that spans are always collected regardless of poll_rate""" + from instana.options import StandardOptions + from instana.span.span import InstanaSpan + from instana.span.registered_span import RegisteredSpan + from instana.span_context import SpanContext + from instana.recorder import StanRecorder + + # Set high poll_rate + self.agent.options = StandardOptions() + self.agent.options.poll_rate = 5 + + with patch.object(gc, "isenabled", return_value=True): + # Create span context and processor + span_context = SpanContext(trace_id=123, span_id=456, is_remote=False) + span_processor = StanRecorder(self.agent) + + # Add a span to the queue + span = InstanaSpan("test-span", span_context, span_processor) + registered_span = RegisteredSpan(span, None, "log") + self.agent.collector.span_queue.put(registered_span) + + # Set metrics_data_last_sent to now (so metrics won't be collected) + from time import time + + self.agent.collector.metrics_data_last_sent = int(time()) + + # Prepare payload + payload = self.agent.collector.prepare_payload() + + # Spans should still be collected + assert payload + assert "spans" in payload + assert len(payload["spans"]) == 1 + + # But metrics should not be collected + assert "metrics" in payload + assert "plugins" in payload["metrics"] + assert len(payload["metrics"]["plugins"]) == 0 + def test_prepare_payload_basics(self) -> None: with patch.object(gc, "isenabled", return_value=True): self.payload = self.agent.collector.prepare_payload() @@ -256,9 +406,9 @@ def test_prepare_payload_with_autowrapt(self) -> None: 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 ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" assert snapshot["versions"]["instana"] == VERSION def test_prepare_payload_with_autotrace(self) -> None: @@ -274,9 +424,9 @@ def test_prepare_payload_with_autotrace(self) -> None: 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 ( + package in snapshot["versions"] + ), f"{package} not found in snapshot['versions']" assert snapshot["versions"]["instana"] == VERSION def test_prepare_and_report_data_without_lock( diff --git a/tests/test_options.py b/tests/test_options.py index f6cd7bc6..d0004e09 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -2,7 +2,7 @@ import logging import os -from typing import Generator +from typing import Generator, Optional import pytest from mock import patch @@ -1003,6 +1003,125 @@ def test_set_from_bool( assert self.standart_options.span_filters == {"exclude": INTERNAL_SPAN_FILTERS} assert not self.standart_options.extra_http_headers + def test_default_poll_rate(self) -> None: + """Test that default poll_rate is 1 second""" + self.standart_options = StandardOptions() + assert self.standart_options.poll_rate == 1 + + @pytest.mark.parametrize( + "poll_rate_value", + [1, 5], + ) + def test_set_from_with_valid_poll_rate( + self, + poll_rate_value: int, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test setting poll_rate from announce response - affects metrics only""" + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + + self.standart_options = StandardOptions() + test_res_data = {"plugin": {"python": {"poll_rate": poll_rate_value}}} + self.standart_options.set_from(test_res_data) + + assert self.standart_options.poll_rate == poll_rate_value + assert ( + f"Poll rate set to {poll_rate_value} seconds from agent configuration" + in caplog.messages + ) + + @pytest.mark.parametrize( + "invalid_value", + [10, 0, -5, 3], + ) + def test_set_from_with_invalid_poll_rate_defaults_to_1( + self, + invalid_value: int, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test that invalid poll_rate values default to 1""" + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + + self.standart_options = StandardOptions() + test_res_data = {"plugin": {"python": {"poll_rate": invalid_value}}} + self.standart_options.set_from(test_res_data) + assert self.standart_options.poll_rate == 1 + assert ( + f"Invalid poll_rate value {invalid_value}, defaulting to 1" + in caplog.messages + ) + + @pytest.mark.parametrize( + "invalid_type,expect_log", + [ + ("invalid", True), + (None, False), + ], + ) + def test_set_from_with_invalid_poll_rate_type( + self, + invalid_type: Optional[str], + expect_log: bool, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test that non-integer poll_rate values default to 1""" + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + + self.standart_options = StandardOptions() + test_res_data = {"plugin": {"python": {"poll_rate": invalid_type}}} + self.standart_options.set_from(test_res_data) + assert self.standart_options.poll_rate == 1 + if expect_log: + assert "Invalid poll_rate type, defaulting to 1" in caplog.messages + + def test_set_from_without_poll_rate(self) -> None: + """Test that poll_rate remains default when not in response""" + self.standart_options = StandardOptions() + test_res_data = { + "secrets": {"matcher": "sample-match", "list": ["sample", "list"]} + } + self.standart_options.set_from(test_res_data) + assert self.standart_options.poll_rate == 1 + + def test_set_from_with_poll_rate_and_other_config( + self, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Test that poll_rate works alongside other configuration""" + caplog.set_level(logging.DEBUG, logger="instana") + caplog.clear() + + self.standart_options = StandardOptions() + test_res_data = { + "plugin": {"python": {"poll_rate": 5}}, + "secrets": {"matcher": "sample-match", "list": ["sample", "list"]}, + "tracing": { + "filter": { + "exclude": [ + { + "name": "service1", + "attributes": [ + { + "key": "service", + "values": ["service1"], + "match_type": "strict", + } + ], + } + ] + } + }, + } + self.standart_options.set_from(test_res_data) + + assert self.standart_options.poll_rate == 5 + assert self.standart_options.secrets_matcher == "sample-match" + assert self.standart_options.secrets_list == ["sample", "list"] + assert "Poll rate set to 5 seconds from agent configuration" in caplog.messages + class TestServerlessOptions: @pytest.fixture(autouse=True)