`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 <riccardo.magliocchetti@gmail.com>
Co-authored-by: Emídio Neto <9735060+emdneto@users.noreply.github.com>
This commit is contained in:
David Gang 2025-08-23 16:15:31 +03:00 committed by GitHub
parent 5fa222f005
commit 973d10d218
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 117 additions and 0 deletions

View File

@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.11.0 - `opentelemetry-instrumentation-confluent-kafka` Add support for confluent-kafka <=2.11.0
([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685)) ([#3685](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3685))
- `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))
## Version 1.36.0/0.57b0 (2025-07-29) ## Version 1.36.0/0.57b0 (2025-07-29)

View File

@ -11,6 +11,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# pylint: disable=too-many-lines
""" """
Instrument to report system (CPU, memory, network) and Instrument to report system (CPU, memory, network) and
process (CPU, memory, garbage collection) metrics. By default, the process (CPU, memory, garbage collection) metrics. By default, the
@ -45,6 +47,8 @@ following metrics are configured:
"process.runtime.cpu.time": ["user", "system"], "process.runtime.cpu.time": ["user", "system"],
"process.runtime.gc_count": None, "process.runtime.gc_count": None,
"cpython.gc.collections": None, "cpython.gc.collections": None,
"cpython.gc.collected_objects": None,
"cpython.gc.uncollectable_objects": None,
"process.runtime.thread_count": None, "process.runtime.thread_count": None,
"process.runtime.cpu.utilization": None, "process.runtime.cpu.utilization": None,
"process.runtime.context_switches": ["involuntary", "voluntary"], "process.runtime.context_switches": ["involuntary", "voluntary"],
@ -138,6 +142,8 @@ _DEFAULT_CONFIG: dict[str, list[str] | None] = {
"process.runtime.cpu.time": ["user", "system"], "process.runtime.cpu.time": ["user", "system"],
"process.runtime.gc_count": None, "process.runtime.gc_count": None,
"cpython.gc.collections": None, "cpython.gc.collections": None,
"cpython.gc.collected_objects": None,
"cpython.gc.uncollectable_objects": None,
"process.runtime.thread_count": None, "process.runtime.thread_count": None,
"process.runtime.cpu.utilization": None, "process.runtime.cpu.utilization": None,
"process.runtime.context_switches": ["involuntary", "voluntary"], "process.runtime.context_switches": ["involuntary", "voluntary"],
@ -199,6 +205,8 @@ class SystemMetricsInstrumentor(BaseInstrumentor):
self._runtime_cpu_time_labels = self._labels.copy() self._runtime_cpu_time_labels = self._labels.copy()
self._runtime_gc_count_labels = self._labels.copy() self._runtime_gc_count_labels = self._labels.copy()
self._runtime_gc_collections_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_thread_count_labels = self._labels.copy()
self._runtime_cpu_utilization_labels = self._labels.copy() self._runtime_cpu_utilization_labels = self._labels.copy()
self._runtime_context_switches_labels = self._labels.copy() self._runtime_context_switches_labels = self._labels.copy()
@ -486,6 +494,32 @@ class SystemMetricsInstrumentor(BaseInstrumentor):
unit="{collection}", unit="{collection}",
) )
if "cpython.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 since interpreter start.",
unit="{object}",
)
if "cpython.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 uncollectable objects found since interpreter start.",
unit="{object}",
)
if "process.runtime.thread_count" in self._config: if "process.runtime.thread_count" in self._config:
self._meter.create_observable_up_down_counter( self._meter.create_observable_up_down_counter(
name=f"process.runtime.{self._python_implementation}.thread_count", name=f"process.runtime.{self._python_implementation}.thread_count",
@ -911,6 +945,32 @@ class SystemMetricsInstrumentor(BaseInstrumentor):
stat["collections"], self._runtime_gc_collections_labels.copy() stat["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"""
for index, stat in enumerate(gc.get_stats()):
self._runtime_gc_collected_objects_labels["generation"] = str(
index
)
yield Observation(
stat["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"""
for index, stat in enumerate(gc.get_stats()):
self._runtime_gc_uncollectable_objects_labels["generation"] = str(
index
)
yield Observation(
stat["uncollectable"],
self._runtime_gc_uncollectable_objects_labels.copy(),
)
def _get_runtime_thread_count( def _get_runtime_thread_count(
self, options: CallbackOptions self, options: CallbackOptions
) -> Iterable[Observation]: ) -> Iterable[Observation]:

View File

@ -139,6 +139,12 @@ class TestSystemMetrics(TestBase):
observer_names.append( observer_names.append(
"cpython.gc.collections", "cpython.gc.collections",
) )
observer_names.append(
"cpython.gc.collected_objects",
)
observer_names.append(
"cpython.gc.uncollectable_objects",
)
if sys.platform != "darwin": if sys.platform != "darwin":
observer_names.append("system.network.connections") observer_names.append("system.network.connections")
@ -983,6 +989,54 @@ class TestSystemMetrics(TestBase):
expected_gc_collections, expected_gc_collections,
) )
@mock.patch("gc.get_stats")
@skipIf(
python_implementation().lower() == "pypy", "not supported for pypy"
)
def test_runtime_get_gc_collected_objects(self, mock_gc_get_stats):
mock_gc_get_stats.configure_mock(
**{
"return_value": [
{"collections": 10, "collected": 100, "uncollectable": 1},
{"collections": 20, "collected": 200, "uncollectable": 2},
{"collections": 30, "collected": 300, "uncollectable": 3},
]
}
)
expected_gc_collected_objects = [
_SystemMetricsResult({"generation": "0"}, 100),
_SystemMetricsResult({"generation": "1"}, 200),
_SystemMetricsResult({"generation": "2"}, 300),
]
self._test_metrics(
"cpython.gc.collected_objects",
expected_gc_collected_objects,
)
@mock.patch("gc.get_stats")
@skipIf(
python_implementation().lower() == "pypy", "not supported for pypy"
)
def test_runtime_get_gc_uncollectable_objects(self, mock_gc_get_stats):
mock_gc_get_stats.configure_mock(
**{
"return_value": [
{"collections": 10, "collected": 100, "uncollectable": 1},
{"collections": 20, "collected": 200, "uncollectable": 2},
{"collections": 30, "collected": 300, "uncollectable": 3},
]
}
)
expected_gc_uncollectable_objects = [
_SystemMetricsResult({"generation": "0"}, 1),
_SystemMetricsResult({"generation": "1"}, 2),
_SystemMetricsResult({"generation": "2"}, 3),
]
self._test_metrics(
"cpython.gc.uncollectable_objects",
expected_gc_uncollectable_objects,
)
@mock.patch("psutil.Process.num_ctx_switches") @mock.patch("psutil.Process.num_ctx_switches")
def test_runtime_context_switches(self, mock_process_num_ctx_switches): def test_runtime_context_switches(self, mock_process_num_ctx_switches):
PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"]) PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"])