Skip to content

Commit 973d10d

Browse files
david-gangxrmxemdneto
authored
opentelemetry-instrumentation-system-metrics: Add cpython.gc.collected_objects and cpython.gc.uncollectable_objects metrics (#3666)
* `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics * Update __init__.py * Update CHANGELOG.md --------- Co-authored-by: Riccardo Magliocchetti <[email protected]> Co-authored-by: Emídio Neto <[email protected]>
1 parent 5fa222f commit 973d10d

File tree

3 files changed

+117
-0
lines changed

3 files changed

+117
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424

2525
- `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.11.0
2626
([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685))
27+
- `opentelemetry-instrumentation-system-metrics`: Add `cpython.gc.collected_objects` and `cpython.gc.uncollectable_objects` metrics
28+
([#3666](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3666))
29+
2730

2831
## Version 1.36.0/0.57b0 (2025-07-29)
2932

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
# pylint: disable=too-many-lines
15+
1416
"""
1517
Instrument to report system (CPU, memory, network) and
1618
process (CPU, memory, garbage collection) metrics. By default, the
@@ -45,6 +47,8 @@
4547
"process.runtime.cpu.time": ["user", "system"],
4648
"process.runtime.gc_count": None,
4749
"cpython.gc.collections": None,
50+
"cpython.gc.collected_objects": None,
51+
"cpython.gc.uncollectable_objects": None,
4852
"process.runtime.thread_count": None,
4953
"process.runtime.cpu.utilization": None,
5054
"process.runtime.context_switches": ["involuntary", "voluntary"],
@@ -138,6 +142,8 @@
138142
"process.runtime.cpu.time": ["user", "system"],
139143
"process.runtime.gc_count": None,
140144
"cpython.gc.collections": None,
145+
"cpython.gc.collected_objects": None,
146+
"cpython.gc.uncollectable_objects": None,
141147
"process.runtime.thread_count": None,
142148
"process.runtime.cpu.utilization": None,
143149
"process.runtime.context_switches": ["involuntary", "voluntary"],
@@ -199,6 +205,8 @@ def __init__(
199205
self._runtime_cpu_time_labels = self._labels.copy()
200206
self._runtime_gc_count_labels = self._labels.copy()
201207
self._runtime_gc_collections_labels = self._labels.copy()
208+
self._runtime_gc_collected_objects_labels = self._labels.copy()
209+
self._runtime_gc_uncollectable_objects_labels = self._labels.copy()
202210
self._runtime_thread_count_labels = self._labels.copy()
203211
self._runtime_cpu_utilization_labels = self._labels.copy()
204212
self._runtime_context_switches_labels = self._labels.copy()
@@ -486,6 +494,32 @@ def _instrument(self, **kwargs: Any):
486494
unit="{collection}",
487495
)
488496

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

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