Skip to content

Commit 6e80f50

Browse files
committed
opentelemetry-instrumentation-system-metrics: Add cpython.gc.collected_objects and cpython.gc.uncollectable_objects metrics
1 parent d2d136d commit 6e80f50

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
1212
## Unreleased
1313

14+
### Added
15+
16+
- `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics ([#3666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3666))
17+
1418
## Version 1.36.0/0.57b0 (2025-07-29)
1519

1620
### Fixed

instrumentation/opentelemetry-instrumentation-system-metrics/src/opentelemetry/instrumentation/system_metrics/__init__.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
"process.runtime.cpu.time": ["user", "system"],
4646
"process.runtime.gc_count": None,
4747
"cpython.gc.collections": None,
48+
"cpython.gc.collected_objects": None,
49+
"cpython.gc.uncollectable_objects": None,
4850
"process.runtime.thread_count": None,
4951
"process.runtime.cpu.utilization": None,
5052
"process.runtime.context_switches": ["involuntary", "voluntary"],
@@ -138,6 +140,8 @@
138140
"process.runtime.cpu.time": ["user", "system"],
139141
"process.runtime.gc_count": None,
140142
"cpython.gc.collections": None,
143+
"cpython.gc.collected_objects": None,
144+
"cpython.gc.uncollectable_objects": None,
141145
"process.runtime.thread_count": None,
142146
"process.runtime.cpu.utilization": None,
143147
"process.runtime.context_switches": ["involuntary", "voluntary"],
@@ -199,6 +203,8 @@ def __init__(
199203
self._runtime_cpu_time_labels = self._labels.copy()
200204
self._runtime_gc_count_labels = self._labels.copy()
201205
self._runtime_gc_collections_labels = self._labels.copy()
206+
self._runtime_gc_collected_objects_labels = self._labels.copy()
207+
self._runtime_gc_uncollectable_objects_labels = self._labels.copy()
202208
self._runtime_thread_count_labels = self._labels.copy()
203209
self._runtime_cpu_utilization_labels = self._labels.copy()
204210
self._runtime_context_switches_labels = self._labels.copy()
@@ -486,6 +492,32 @@ def _instrument(self, **kwargs: Any):
486492
unit="{collection}",
487493
)
488494

495+
if "cpython.gc.collected_objects" in self._config:
496+
if self._python_implementation == "pypy":
497+
_logger.warning(
498+
"The cpython.gc.collected_objects metric won't be collected because the interpreter is PyPy"
499+
)
500+
else:
501+
self._meter.create_observable_counter(
502+
name="cpython.gc.collected_objects",
503+
callbacks=[self._get_runtime_gc_collected_objects],
504+
description="The total number of objects collected since interpreter start.",
505+
unit="{object}",
506+
)
507+
508+
if "cpython.gc.uncollectable_objects" in self._config:
509+
if self._python_implementation == "pypy":
510+
_logger.warning(
511+
"The cpython.gc.uncollectable_objects metric won't be collected because the interpreter is PyPy"
512+
)
513+
else:
514+
self._meter.create_observable_counter(
515+
name="cpython.gc.uncollectable_objects",
516+
callbacks=[self._get_runtime_gc_uncollectable_objects],
517+
description="The total number of uncollectable objects found since interpreter start.",
518+
unit="{object}",
519+
)
520+
489521
if "process.runtime.thread_count" in self._config:
490522
self._meter.create_observable_up_down_counter(
491523
name=f"process.runtime.{self._python_implementation}.thread_count",
@@ -911,6 +943,32 @@ def _get_runtime_gc_collections(
911943
stat["collections"], self._runtime_gc_collections_labels.copy()
912944
)
913945

946+
def _get_runtime_gc_collected_objects(
947+
self, options: CallbackOptions
948+
) -> Iterable[Observation]:
949+
"""Observer callback for garbage collection collected objects"""
950+
for index, stat in enumerate(gc.get_stats()):
951+
self._runtime_gc_collected_objects_labels["generation"] = str(
952+
index
953+
)
954+
yield Observation(
955+
stat["collected"],
956+
self._runtime_gc_collected_objects_labels.copy(),
957+
)
958+
959+
def _get_runtime_gc_uncollectable_objects(
960+
self, options: CallbackOptions
961+
) -> Iterable[Observation]:
962+
"""Observer callback for garbage collection uncollectable objects"""
963+
for index, stat in enumerate(gc.get_stats()):
964+
self._runtime_gc_uncollectable_objects_labels["generation"] = str(
965+
index
966+
)
967+
yield Observation(
968+
stat["uncollectable"],
969+
self._runtime_gc_uncollectable_objects_labels.copy(),
970+
)
971+
914972
def _get_runtime_thread_count(
915973
self, options: CallbackOptions
916974
) -> Iterable[Observation]:

instrumentation/opentelemetry-instrumentation-system-metrics/tests/test_system_metrics.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,12 @@ def test_system_metrics_instrument(self):
139139
observer_names.append(
140140
"cpython.gc.collections",
141141
)
142+
observer_names.append(
143+
"cpython.gc.collected_objects",
144+
)
145+
observer_names.append(
146+
"cpython.gc.uncollectable_objects",
147+
)
142148
if sys.platform != "darwin":
143149
observer_names.append("system.network.connections")
144150

@@ -983,6 +989,54 @@ def test_runtime_get_gc_collections(self, mock_gc_get_stats):
983989
expected_gc_collections,
984990
)
985991

992+
@mock.patch("gc.get_stats")
993+
@skipIf(
994+
python_implementation().lower() == "pypy", "not supported for pypy"
995+
)
996+
def test_runtime_get_gc_collected_objects(self, mock_gc_get_stats):
997+
mock_gc_get_stats.configure_mock(
998+
**{
999+
"return_value": [
1000+
{"collections": 10, "collected": 100, "uncollectable": 1},
1001+
{"collections": 20, "collected": 200, "uncollectable": 2},
1002+
{"collections": 30, "collected": 300, "uncollectable": 3},
1003+
]
1004+
}
1005+
)
1006+
expected_gc_collected_objects = [
1007+
_SystemMetricsResult({"generation": "0"}, 100),
1008+
_SystemMetricsResult({"generation": "1"}, 200),
1009+
_SystemMetricsResult({"generation": "2"}, 300),
1010+
]
1011+
self._test_metrics(
1012+
"cpython.gc.collected_objects",
1013+
expected_gc_collected_objects,
1014+
)
1015+
1016+
@mock.patch("gc.get_stats")
1017+
@skipIf(
1018+
python_implementation().lower() == "pypy", "not supported for pypy"
1019+
)
1020+
def test_runtime_get_gc_uncollectable_objects(self, mock_gc_get_stats):
1021+
mock_gc_get_stats.configure_mock(
1022+
**{
1023+
"return_value": [
1024+
{"collections": 10, "collected": 100, "uncollectable": 1},
1025+
{"collections": 20, "collected": 200, "uncollectable": 2},
1026+
{"collections": 30, "collected": 300, "uncollectable": 3},
1027+
]
1028+
}
1029+
)
1030+
expected_gc_uncollectable_objects = [
1031+
_SystemMetricsResult({"generation": "0"}, 1),
1032+
_SystemMetricsResult({"generation": "1"}, 2),
1033+
_SystemMetricsResult({"generation": "2"}, 3),
1034+
]
1035+
self._test_metrics(
1036+
"cpython.gc.uncollectable_objects",
1037+
expected_gc_uncollectable_objects,
1038+
)
1039+
9861040
@mock.patch("psutil.Process.num_ctx_switches")
9871041
def test_runtime_context_switches(self, mock_process_num_ctx_switches):
9881042
PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"])

0 commit comments

Comments
 (0)