diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ec01923b..1f4a2b2857 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `opentelemetry-instrumentation-system-metrics`: fix loading on Google Cloud Run + ([#3533](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3533)) - `opentelemetry-instrumentation-fastapi`: fix wrapping of middlewares ([#3012](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3012)) - `opentelemetry-instrumentation-urllib3`: proper bucket boundaries in stable semconv http duration metrics diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py index e0407d8ab4..ff716502fe 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py @@ -395,7 +395,10 @@ def _instrument(self, **kwargs: Any): self._meter, callbacks=[self._get_cpu_utilization] ) - if "process.context_switches" in self._config: + if ( + "process.context_switches" in self._config + and self._can_read_context_switches() + ): self._meter.create_observable_counter( name="process.context_switches", callbacks=[self._get_context_switches], @@ -482,7 +485,10 @@ def _instrument(self, **kwargs: Any): unit="1", ) - if "process.runtime.context_switches" in self._config: + if ( + "process.runtime.context_switches" in self._config + and self._can_read_context_switches() + ): self._meter.create_observable_counter( name=f"process.runtime.{self._python_implementation}.context_switches", callbacks=[self._get_runtime_context_switches], @@ -493,6 +499,14 @@ def _instrument(self, **kwargs: Any): def _uninstrument(self, **kwargs: Any): pass + def _can_read_context_switches(self) -> bool: + """On Google Cloud Run psutil is not able to read context switches, catch it before creating the observable instrument""" + try: + self._proc.num_ctx_switches() + return True + except NotImplementedError: + return False + def _get_open_file_descriptors( self, options: CallbackOptions ) -> Iterable[Observation]: diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py index c5a600218b..975f9b907c 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -# pylint: disable=protected-access +# pylint: disable=protected-access,too-many-lines import sys from collections import namedtuple @@ -235,14 +235,28 @@ def _assert_metrics(self, observer_name, reader, expected): assertions += 1 self.assertEqual(len(expected), assertions) - def _test_metrics(self, observer_name, expected): + @staticmethod + def _setup_instrumentor() -> InMemoryMetricReader: reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) system_metrics = SystemMetricsInstrumentor() system_metrics.instrument(meter_provider=meter_provider) + return reader + + def _test_metrics(self, observer_name, expected): + reader = self._setup_instrumentor() self._assert_metrics(observer_name, reader, expected) + def _assert_metrics_not_found(self, observer_name): + reader = self._setup_instrumentor() + seen_metrics = set() + for resource_metrics in reader.get_metrics_data().resource_metrics: + for scope_metrics in resource_metrics.scope_metrics: + for metric in scope_metrics.metrics: + seen_metrics.add(metric.name) + self.assertNotIn(observer_name, seen_metrics) + # This patch is added here to stop psutil from raising an exception # because we're patching cpu_times # pylint: disable=unused-argument @@ -855,6 +869,14 @@ def test_context_switches(self, mock_process_num_ctx_switches): ] self._test_metrics("process.context_switches", expected) + @mock.patch("psutil.Process.num_ctx_switches") + def test_context_switches_not_implemented_error( + self, mock_process_num_ctx_switches + ): + mock_process_num_ctx_switches.side_effect = NotImplementedError + + self._assert_metrics_not_found("process.context_switches") + @mock.patch("psutil.Process.num_threads") def test_thread_count(self, mock_process_thread_num): mock_process_thread_num.configure_mock(**{"return_value": 42}) @@ -947,6 +969,16 @@ def test_runtime_context_switches(self, mock_process_num_ctx_switches): f"process.runtime.{self.implementation}.context_switches", expected ) + @mock.patch("psutil.Process.num_ctx_switches") + def test_runtime_context_switches_not_implemented_error( + self, mock_process_num_ctx_switches + ): + mock_process_num_ctx_switches.side_effect = NotImplementedError + + self._assert_metrics_not_found( + f"process.runtime.{self.implementation}.context_switches", + ) + @mock.patch("psutil.Process.num_threads") def test_runtime_thread_count(self, mock_process_thread_num): mock_process_thread_num.configure_mock(**{"return_value": 42})