diff --git a/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt index c62da59804..fe7582a2bb 100644 --- a/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt +++ b/instrumentation/opentelemetry-instrumentation-aiohttp-server/test-requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.9.3 +aiohttp==3.9.4 aiosignal==1.3.1 asgiref==3.7.2 async-timeout==4.0.3 diff --git a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml index 07ba2faa20..744461713d 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ dependencies = [ "opentelemetry-instrumentation == 0.47b0.dev", "opentelemetry-api ~= 1.11", + "opentelemetry-semantic-conventions ~= 0.48b0", "psutil ~= 5.9", ] 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 6342d287d5..55c1bfcc3f 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 @@ -36,7 +36,9 @@ "system.thread_count": None "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], - "process.runtime.gc_count": None, + "process.runtime.gc.collections": None, + "process.runtime.gc.collected_objects": None, + "process.runtime.gc.uncollectable_objects": None, "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], @@ -91,6 +93,14 @@ from opentelemetry.instrumentation.system_metrics.package import _instruments from opentelemetry.instrumentation.system_metrics.version import __version__ from opentelemetry.metrics import CallbackOptions, Observation, get_meter +from opentelemetry.semconv._incubating.attributes.cpython_attributes import ( + CPYTHON_GC_GENERATION, +) +from opentelemetry.semconv._incubating.metrics.cpython_metrics import ( + CPYTHON_GC_COLLECTED_OBJECTS, + CPYTHON_GC_COLLECTIONS, + CPYTHON_GC_UNCOLLECTABLE_OBJECTS, +) _logger = logging.getLogger(__name__) @@ -113,7 +123,9 @@ "system.thread_count": None, "process.runtime.memory": ["rss", "vms"], "process.runtime.cpu.time": ["user", "system"], - "process.runtime.gc_count": None, + "process.runtime.gc.collections": None, + "process.runtime.gc.collected_objects": None, + "process.runtime.gc.uncollectable_objects": None, "process.runtime.thread_count": None, "process.runtime.cpu.utilization": None, "process.runtime.context_switches": ["involuntary", "voluntary"], @@ -165,7 +177,9 @@ 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_gc_collected_objects_labels = self._labels.copy() + self._runtime_gc_uncollectable_objects_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() @@ -359,17 +373,43 @@ def _instrument(self, **kwargs): unit="seconds", ) - if "process.runtime.gc_count" in self._config: + if "process.runtime.gc.collections" in self._config: if self._python_implementation == "pypy": _logger.warning( - "The process.runtime.gc_count metric won't be collected because the interpreter is PyPy" + "The cpython.gc.collections metric won't be collected because the interpreter is PyPy" ) else: self._meter.create_observable_counter( - name=f"process.runtime.{self._python_implementation}.gc_count", - callbacks=[self._get_runtime_gc_count], - description=f"Runtime {self._python_implementation} GC count", - unit="bytes", + 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.gc.collected_objects" in self._config: + if self._python_implementation == "pypy": + _logger.warning( + "The cpython.gc.collected_objects metric won't be collected because the interpreter is PyPy" + ) + else: + self._meter.create_observable_counter( + name=CPYTHON_GC_COLLECTED_OBJECTS, + callbacks=[self._get_runtime_gc_collected_objects], + description="The total number of objects collected inside a generation since interpreter start", + unit="{object}", + ) + + if "process.runtime.gc.uncollectable_objects" in self._config: + if self._python_implementation == "pypy": + _logger.warning( + "The cpython.gc.uncollectable_objects metric won't be collected because the interpreter is PyPy" + ) + else: + self._meter.create_observable_counter( + name=CPYTHON_GC_UNCOLLECTABLE_OBJECTS, + callbacks=[self._get_runtime_gc_uncollectable_objects], + description="The total number of objects which were found to be uncollectable inside a generation since interpreter start", + unit="{object}", ) if "process.runtime.thread_count" in self._config: @@ -689,13 +729,44 @@ def _get_runtime_cpu_time( self._runtime_cpu_time_labels.copy(), ) - def _get_runtime_gc_count( + 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_count_labels["count"] = str(index) - yield Observation(count, self._runtime_gc_count_labels.copy()) + """Observer callback for garbage collection collections count""" + for generation, stats in enumerate(gc.get_stats()): + self._runtime_gc_collections_labels[CPYTHON_GC_GENERATION] = ( + generation + ) + yield Observation( + stats["collections"], + self._runtime_gc_collections_labels.copy(), + ) + + def _get_runtime_gc_collected_objects( + self, options: CallbackOptions + ) -> Iterable[Observation]: + """Observer callback for garbage collection collected objects count""" + for generation, stats in enumerate(gc.get_stats()): + self._runtime_gc_collected_objects_labels[CPYTHON_GC_GENERATION] = ( + generation + ) + yield Observation( + stats["collected"], + self._runtime_gc_collected_objects_labels.copy(), + ) + + def _get_runtime_gc_uncollectable_objects( + self, options: CallbackOptions + ) -> Iterable[Observation]: + """Observer callback for garbage collection uncollectable objects count""" + for generation, stats in enumerate(gc.get_stats()): + self._runtime_gc_uncollectable_objects_labels[ + CPYTHON_GC_GENERATION + ] = generation + yield Observation( + stats["uncollectable"], + self._runtime_gc_uncollectable_objects_labels.copy(), + ) def _get_runtime_thread_count( self, options: CallbackOptions 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 3986a32c16..5b5273e563 100644 --- a/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py +++ b/instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py @@ -123,10 +123,14 @@ def test_system_metrics_instrument(self): if self.implementation == "pypy": self.assertEqual(len(metric_names), 20) else: - self.assertEqual(len(metric_names), 21) - observer_names.append( - f"process.runtime.{self.implementation}.gc_count", - ) + self.assertEqual(len(metric_names), 23) + observer_names.extend( + [ + "cpython.gc.collections", + "cpython.gc.collected_objects", + "cpython.gc.uncollectable_objects", + ] + ) for observer in metric_names: self.assertIn(observer, observer_names) @@ -142,7 +146,9 @@ def test_runtime_metrics_instrument(self): } if self.implementation != "pypy": - runtime_config["process.runtime.gc_count"] = None + runtime_config["process.runtime.gc.collections"] = None + runtime_config["process.runtime.gc.collected_objects"] = None + runtime_config["process.runtime.gc.uncollectable_objects"] = None reader = InMemoryMetricReader() meter_provider = MeterProvider(metric_readers=[reader]) @@ -166,10 +172,14 @@ def test_runtime_metrics_instrument(self): if self.implementation == "pypy": self.assertEqual(len(metric_names), 5) else: - self.assertEqual(len(metric_names), 6) - observer_names.append( - f"process.runtime.{self.implementation}.gc_count" - ) + self.assertEqual(len(metric_names), 8) + observer_names.extend( + [ + "cpython.gc.collections", + "cpython.gc.collected_objects", + "cpython.gc.uncollectable_objects", + ] + ) for observer in metric_names: self.assertIn(observer, observer_names) @@ -793,22 +803,72 @@ def test_runtime_cpu_time(self, mock_process_cpu_times): f"process.runtime.{self.implementation}.cpu_time", expected ) - @mock.patch("gc.get_count") + @mock.patch("gc.get_stats") @skipIf( python_implementation().lower() == "pypy", "not supported for pypy" ) - def test_runtime_get_count(self, mock_gc_get_count): - mock_gc_get_count.configure_mock(**{"return_value": (1, 2, 3)}) + def test_runtime_gc_collections(self, mock_gc_get_stats): + mock_gc_get_stats.configure_mock( + **{ + "return_value": [ + {"collections": 10, "collected": 100, "uncollectable": 0}, + {"collections": 5, "collected": 50, "uncollectable": 0}, + {"collections": 2, "collected": 20, "uncollectable": 0}, + ] + } + ) expected = [ - _SystemMetricsResult({"count": "0"}, 1), - _SystemMetricsResult({"count": "1"}, 2), - _SystemMetricsResult({"count": "2"}, 3), + _SystemMetricsResult({"cpython.gc.generation": 0}, 10), + _SystemMetricsResult({"cpython.gc.generation": 1}, 5), + _SystemMetricsResult({"cpython.gc.generation": 2}, 2), ] - self._test_metrics( - f"process.runtime.{self.implementation}.gc_count", expected + self._test_metrics("cpython.gc.collections", expected) + + @mock.patch("gc.get_stats") + @skipIf( + python_implementation().lower() == "pypy", "not supported for pypy" + ) + def test_runtime_gc_collected_objects(self, mock_gc_get_stats): + mock_gc_get_stats.configure_mock( + **{ + "return_value": [ + {"collections": 10, "collected": 100, "uncollectable": 0}, + {"collections": 5, "collected": 50, "uncollectable": 0}, + {"collections": 2, "collected": 20, "uncollectable": 0}, + ] + } ) + expected = [ + _SystemMetricsResult({"cpython.gc.generation": 0}, 100), + _SystemMetricsResult({"cpython.gc.generation": 1}, 50), + _SystemMetricsResult({"cpython.gc.generation": 2}, 20), + ] + self._test_metrics("cpython.gc.collected_objects", expected) + + @mock.patch("gc.get_stats") + @skipIf( + python_implementation().lower() == "pypy", "not supported for pypy" + ) + def test_runtime_gc_uncollectable_objects(self, mock_gc_get_stats): + mock_gc_get_stats.configure_mock( + **{ + "return_value": [ + {"collections": 10, "collected": 100, "uncollectable": 1}, + {"collections": 5, "collected": 50, "uncollectable": 2}, + {"collections": 2, "collected": 20, "uncollectable": 3}, + ] + } + ) + + expected = [ + _SystemMetricsResult({"cpython.gc.generation": 0}, 1), + _SystemMetricsResult({"cpython.gc.generation": 1}, 2), + _SystemMetricsResult({"cpython.gc.generation": 2}, 3), + ] + self._test_metrics("cpython.gc.uncollectable_objects", expected) + @mock.patch("psutil.Process.num_ctx_switches") def test_runtime_context_switches(self, mock_process_num_ctx_switches): PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"])