Skip to content

Commit bb42e04

Browse files
authored
Feature/add new process metrics (#1948)
1 parent 7ac6744 commit bb42e04

File tree

3 files changed

+123
-8
lines changed

3 files changed

+123
-8
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
11+
### Added
12+
- `opentelemetry-instrumentation-system-metrics` Add support for collecting process metrics
13+
([#1948](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1948))
14+
1015
### Fixed
1116

1217
- Fix version of Flask dependency `werkzeug`
1318
([#1980](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1980))
1419

20+
1521
## Version 1.20.0/0.41b0 (2023-09-01)
1622

1723
### Fixed

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

+65
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
"system.thread_count": None
3737
"process.runtime.memory": ["rss", "vms"],
3838
"process.runtime.cpu.time": ["user", "system"],
39+
"process.runtime.gc_count": None,
40+
"process.runtime.thread_count": None,
41+
"process.runtime.cpu.utilization": None,
42+
"process.runtime.context_switches": ["involuntary", "voluntary"],
3943
}
4044
4145
Usage
@@ -63,6 +67,7 @@
6367
"system.network.io": ["transmit", "receive"],
6468
"process.runtime.memory": ["rss", "vms"],
6569
"process.runtime.cpu.time": ["user", "system"],
70+
"process.runtime.context_switches": ["involuntary", "voluntary"],
6671
}
6772
SystemMetricsInstrumentor(config=configuration).instrument()
6873
@@ -105,6 +110,9 @@
105110
"process.runtime.memory": ["rss", "vms"],
106111
"process.runtime.cpu.time": ["user", "system"],
107112
"process.runtime.gc_count": None,
113+
"process.runtime.thread_count": None,
114+
"process.runtime.cpu.utilization": None,
115+
"process.runtime.context_switches": ["involuntary", "voluntary"],
108116
}
109117

110118

@@ -150,6 +158,9 @@ def __init__(
150158
self._runtime_memory_labels = self._labels.copy()
151159
self._runtime_cpu_time_labels = self._labels.copy()
152160
self._runtime_gc_count_labels = self._labels.copy()
161+
self._runtime_thread_count_labels = self._labels.copy()
162+
self._runtime_cpu_utilization_labels = self._labels.copy()
163+
self._runtime_context_switches_labels = self._labels.copy()
153164

154165
def instrumentation_dependencies(self) -> Collection[str]:
155166
return _instruments
@@ -347,6 +358,29 @@ def _instrument(self, **kwargs):
347358
unit="bytes",
348359
)
349360

361+
if "process.runtime.thread_count" in self._config:
362+
self._meter.create_observable_up_down_counter(
363+
name=f"process.runtime.{self._python_implementation}.thread_count",
364+
callbacks=[self._get_runtime_thread_count],
365+
description="Runtime active threads count",
366+
)
367+
368+
if "process.runtime.cpu.utilization" in self._config:
369+
self._meter.create_observable_gauge(
370+
name=f"process.runtime.{self._python_implementation}.cpu.utilization",
371+
callbacks=[self._get_runtime_cpu_utilization],
372+
description="Runtime CPU utilization",
373+
unit="1",
374+
)
375+
376+
if "process.runtime.context_switches" in self._config:
377+
self._meter.create_observable_counter(
378+
name=f"process.runtime.{self._python_implementation}.context_switches",
379+
callbacks=[self._get_runtime_context_switches],
380+
description="Runtime context switches",
381+
unit="switches",
382+
)
383+
350384
def _uninstrument(self, **__):
351385
pass
352386

@@ -646,3 +680,34 @@ def _get_runtime_gc_count(
646680
for index, count in enumerate(gc.get_count()):
647681
self._runtime_gc_count_labels["count"] = str(index)
648682
yield Observation(count, self._runtime_gc_count_labels.copy())
683+
684+
def _get_runtime_thread_count(
685+
self, options: CallbackOptions
686+
) -> Iterable[Observation]:
687+
"""Observer callback for runtime active thread count"""
688+
yield Observation(
689+
self._proc.num_threads(), self._runtime_thread_count_labels.copy()
690+
)
691+
692+
def _get_runtime_cpu_utilization(
693+
self, options: CallbackOptions
694+
) -> Iterable[Observation]:
695+
"""Observer callback for runtime CPU utilization"""
696+
proc_cpu_percent = self._proc.cpu_percent()
697+
yield Observation(
698+
proc_cpu_percent,
699+
self._runtime_cpu_utilization_labels.copy(),
700+
)
701+
702+
def _get_runtime_context_switches(
703+
self, options: CallbackOptions
704+
) -> Iterable[Observation]:
705+
"""Observer callback for runtime context switches"""
706+
ctx_switches = self._proc.num_ctx_switches()
707+
for metric in self._config["process.runtime.context_switches"]:
708+
if hasattr(ctx_switches, metric):
709+
self._runtime_context_switches_labels["type"] = metric
710+
yield Observation(
711+
getattr(ctx_switches, metric),
712+
self._runtime_context_switches_labels.copy(),
713+
)

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

+52-8
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
from platform import python_implementation
1919
from unittest import mock
2020

21-
from opentelemetry.instrumentation.system_metrics import (
22-
SystemMetricsInstrumentor,
23-
)
2421
from opentelemetry.sdk.metrics import MeterProvider
2522
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
2623
from opentelemetry.test.test_base import TestBase
2724

25+
from opentelemetry.instrumentation.system_metrics import (
26+
SystemMetricsInstrumentor,
27+
)
28+
2829

2930
def _mock_netconnection():
3031
NetConnection = namedtuple(
@@ -96,7 +97,7 @@ def test_system_metrics_instrument(self):
9697
for scope_metrics in resource_metrics.scope_metrics:
9798
for metric in scope_metrics.metrics:
9899
metric_names.append(metric.name)
99-
self.assertEqual(len(metric_names), 18)
100+
self.assertEqual(len(metric_names), 21)
100101

101102
observer_names = [
102103
"system.cpu.time",
@@ -117,6 +118,9 @@ def test_system_metrics_instrument(self):
117118
f"process.runtime.{self.implementation}.memory",
118119
f"process.runtime.{self.implementation}.cpu_time",
119120
f"process.runtime.{self.implementation}.gc_count",
121+
f"process.runtime.{self.implementation}.thread_count",
122+
f"process.runtime.{self.implementation}.context_switches",
123+
f"process.runtime.{self.implementation}.cpu.utilization",
120124
]
121125

122126
for observer in metric_names:
@@ -128,6 +132,9 @@ def test_runtime_metrics_instrument(self):
128132
"process.runtime.memory": ["rss", "vms"],
129133
"process.runtime.cpu.time": ["user", "system"],
130134
"process.runtime.gc_count": None,
135+
"process.runtime.thread_count": None,
136+
"process.runtime.cpu.utilization": None,
137+
"process.runtime.context_switches": ["involuntary", "voluntary"],
131138
}
132139

133140
reader = InMemoryMetricReader()
@@ -140,12 +147,15 @@ def test_runtime_metrics_instrument(self):
140147
for scope_metrics in resource_metrics.scope_metrics:
141148
for metric in scope_metrics.metrics:
142149
metric_names.append(metric.name)
143-
self.assertEqual(len(metric_names), 3)
150+
self.assertEqual(len(metric_names), 6)
144151

145152
observer_names = [
146153
f"process.runtime.{self.implementation}.memory",
147154
f"process.runtime.{self.implementation}.cpu_time",
148155
f"process.runtime.{self.implementation}.gc_count",
156+
f"process.runtime.{self.implementation}.thread_count",
157+
f"process.runtime.{self.implementation}.context_switches",
158+
f"process.runtime.{self.implementation}.cpu.utilization",
149159
]
150160

151161
for observer in metric_names:
@@ -161,9 +171,9 @@ def _assert_metrics(self, observer_name, reader, expected):
161171
for data_point in metric.data.data_points:
162172
for expect in expected:
163173
if (
164-
dict(data_point.attributes)
165-
== expect.attributes
166-
and metric.name == observer_name
174+
dict(data_point.attributes)
175+
== expect.attributes
176+
and metric.name == observer_name
167177
):
168178
self.assertEqual(
169179
data_point.value,
@@ -782,3 +792,37 @@ def test_runtime_get_count(self, mock_gc_get_count):
782792
self._test_metrics(
783793
f"process.runtime.{self.implementation}.gc_count", expected
784794
)
795+
796+
@mock.patch("psutil.Process.num_ctx_switches")
797+
def test_runtime_context_switches(self, mock_process_num_ctx_switches):
798+
PCtxSwitches = namedtuple("PCtxSwitches", ["voluntary", "involuntary"])
799+
800+
mock_process_num_ctx_switches.configure_mock(
801+
**{"return_value": PCtxSwitches(voluntary=1, involuntary=2)}
802+
)
803+
804+
expected = [
805+
_SystemMetricsResult({"type": "voluntary"}, 1),
806+
_SystemMetricsResult({"type": "involuntary"}, 2),
807+
]
808+
self._test_metrics(
809+
f"process.runtime.{self.implementation}.context_switches", expected
810+
)
811+
812+
@mock.patch("psutil.Process.num_threads")
813+
def test_runtime_thread_num(self, mock_process_thread_num):
814+
mock_process_thread_num.configure_mock(**{"return_value": 42})
815+
816+
expected = [_SystemMetricsResult({}, 42)]
817+
self._test_metrics(
818+
f"process.runtime.{self.implementation}.thread_count", expected
819+
)
820+
821+
@mock.patch("psutil.Process.cpu_percent")
822+
def test_runtime_cpu_percent(self, mock_process_cpu_percent):
823+
mock_process_cpu_percent.configure_mock(**{"return_value": 42})
824+
825+
expected = [_SystemMetricsResult({}, 42)]
826+
self._test_metrics(
827+
f"process.runtime.{self.implementation}.cpu.utilization", expected
828+
)

0 commit comments

Comments
 (0)