diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e74a94985..1f252e8290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610)) - `opentelemetry-instrumentation-kafka-python` Utilize instruments-any functionality. ([#3610](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3610)) +- `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collections` metrics with collection unit is specified in semconv ([3617](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3617)) ## Version 1.35.0/0.56b0 (2025-07-11) 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 ff716502fe..7944517fce 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 @@ -44,6 +44,7 @@ "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], "process.runtime.gc_count": None, + "cpython.gc.collections": None, "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], @@ -136,6 +137,7 @@ "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], "process.runtime.gc_count": None, + "cpython.gc.collections": None, "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], @@ -196,6 +198,7 @@ def __init__( self._runtime_memory_labels = self._labels.copy() self._runtime_cpu_time_labels = self._labels.copy() self._runtime_gc_count_labels = self._labels.copy() + self._runtime_gc_collections_labels = self._labels.copy() self._runtime_thread_count_labels = self._labels.copy() self._runtime_cpu_utilization_labels = self._labels.copy() self._runtime_context_switches_labels = self._labels.copy() @@ -470,6 +473,19 @@ def _instrument(self, **kwargs: Any): unit="By", ) + if "cpython.gc.collections" in self._config: + if self._python_implementation == "pypy": + _logger.warning( + "The cpython.gc.collections metric won't be collected because the interpreter is PyPy" + ) + else: + self._meter.create_observable_counter( + name="cpython.gc.collections", + callbacks=[self._get_runtime_gc_collections], + description="The number of times a generation was collected since interpreter start.", + unit="{collection}", + ) + if "process.runtime.thread_count" in self._config: self._meter.create_observable_up_down_counter( name=f"process.runtime.{self._python_implementation}.thread_count", @@ -885,6 +901,16 @@ def _get_runtime_gc_count( self._runtime_gc_count_labels["count"] = str(index) yield Observation(count, self._runtime_gc_count_labels.copy()) + def _get_runtime_gc_collections( + self, options: CallbackOptions + ) -> Iterable[Observation]: + """Observer callback for garbage collection""" + for index, count in enumerate(gc.get_count()): + self._runtime_gc_collections_labels["generation"] = str(index) + yield Observation( + count, self._runtime_gc_collections_labels.copy() + ) + def _get_runtime_thread_count( 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 157322fb4b..07d95b0b47 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -136,6 +136,9 @@ def test_system_metrics_instrument(self): observer_names.append( f"process.runtime.{self.implementation}.gc_count", ) + observer_names.append( + "cpython.gc.collections", + ) if sys.platform != "darwin": observer_names.append("system.network.connections") @@ -946,13 +949,30 @@ def test_runtime_cpu_time(self, mock_process_cpu_times): def test_runtime_get_count(self, mock_gc_get_count): mock_gc_get_count.configure_mock(**{"return_value": (1, 2, 3)}) - expected = [ + expected_gc_count = [ _SystemMetricsResult({"count": "0"}, 1), _SystemMetricsResult({"count": "1"}, 2), _SystemMetricsResult({"count": "2"}, 3), ] self._test_metrics( - f"process.runtime.{self.implementation}.gc_count", expected + f"process.runtime.{self.implementation}.gc_count", + expected_gc_count, + ) + + @mock.patch("gc.get_count") + @skipIf( + python_implementation().lower() == "pypy", "not supported for pypy" + ) + def test_runtime_get_gc_collections(self, mock_gc_get_count): + mock_gc_get_count.configure_mock(**{"return_value": (1, 2, 3)}) + expected_gc_collections = [ + _SystemMetricsResult({"generation": "0"}, 1), + _SystemMetricsResult({"generation": "1"}, 2), + _SystemMetricsResult({"generation": "2"}, 3), + ] + self._test_metrics( + "cpython.gc.collections", + expected_gc_collections, ) @mock.patch("psutil.Process.num_ctx_switches")