`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:
parent
5fa222f005
commit
973d10d218
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
|
|
@ -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"])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue