Skip to content
This repository was archived by the owner on Sep 2, 2022. It is now read-only.

Commit 02ece92

Browse files
authored
Merge pull request #16 from apilytics/cpu-usage
Send current system CPU usage together with metrics on Linux
2 parents fc117da + 2443cb8 commit 02ece92

6 files changed

Lines changed: 283 additions & 8 deletions

File tree

CHANGELOG.md

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

1010
### Added
1111

12+
- Send current system CPU usage together with metrics on Linux systems.
1213
- Send current system memory usage and total available memory together with metrics on Linux systems.
1314
- Add platform name to sent Apilytics version info.
1415

apilytics/core.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def set_response_info(
129129

130130
def _send_metrics(self) -> None:
131131
memory_usage, memory_total = _get_used_and_total_memory()
132+
cpu_usage = _get_cpu_usage()
132133

133134
request = urllib.request.Request(
134135
url="https://www.apilytics.io/api/v1/middleware",
@@ -161,6 +162,7 @@ def _send_metrics(self) -> None:
161162
if self._response_size is not None
162163
else {}
163164
),
165+
**({"cpuUsage": cpu_usage} if cpu_usage is not None else {}),
164166
**({"memoryUsage": memory_usage} if memory_usage is not None else {}),
165167
**({"memoryTotal": memory_total} if memory_total is not None else {}),
166168
}
@@ -170,6 +172,59 @@ def _send_metrics(self) -> None:
170172
pass
171173

172174

175+
def _get_cpu_usage() -> Optional[float]:
176+
"""
177+
Get the current CPU usage as a percentage.
178+
179+
Returns:
180+
A percentage value between 0 and 1, or None if the CPU usage could not be
181+
determined (most likely because the system is not Linux).
182+
"""
183+
if platform.system() != "Linux":
184+
return None
185+
186+
def cpu_times() -> Tuple[int, int]:
187+
with open("/proc/stat") as f:
188+
stat = f.readline()
189+
190+
# Ignore the `cpu` text from the start and the last two "guest" times.
191+
times = [int(val) for val in stat.split()[1:9]]
192+
193+
total = sum(times)
194+
idle = times[3]
195+
196+
try:
197+
# Include `iowait` time into idle time if available, as does:
198+
# https://github.com/torvalds/linux/blob/4f12b742eb2b3a850ac8be7dc4ed52976fc6cb0b/kernel/sched/cputime.c#L225
199+
idle += times[4]
200+
except IndexError:
201+
# `iowait` time is not available before Linux 2.5.41, quite unlikely
202+
# to happen but doesn't hurt to handle this anyway.
203+
pass
204+
205+
return idle, total
206+
207+
try:
208+
idle_start, total_start = cpu_times()
209+
210+
# There is no such thing as CPU usage percentage on a single point of time.
211+
# At any discrete instant a CPU core is either fully used or fully idle.
212+
# This is why we need to measure the usage over a known time interval. An
213+
# interval of one second has been tested to provide quite consistent results.
214+
time.sleep(1)
215+
216+
idle_end, total_end = cpu_times()
217+
except OSError:
218+
return None
219+
220+
try:
221+
idle_percentage = (idle_end - idle_start) / (total_end - total_start)
222+
except ZeroDivisionError:
223+
return 0.0
224+
225+
return 1 - idle_percentage
226+
227+
173228
def _get_used_and_total_memory() -> Tuple[Optional[int], Optional[int]]:
174229
"""
175230
Get information about the used and total system memory.

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ def mocked_executor() -> Generator[None, None, None]:
3535
new=_MockedExecutor,
3636
):
3737
yield
38+
39+
40+
@pytest.fixture(scope="session", autouse=True)
41+
def mocked_sleep() -> Generator[None, None, None]:
42+
with unittest.mock.patch("apilytics.core.time.sleep", new=lambda secs: None):
43+
yield

tests/django/test_django.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ def test_middleware_should_call_apilytics_api(
3939
"requestSize",
4040
"responseSize",
4141
"timeMillis",
42-
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
42+
*(
43+
("cpuUsage", "memoryUsage", "memoryTotal")
44+
if platform.system() == "Linux"
45+
else ()
46+
),
4347
}
4448
assert data["path"] == "/"
4549
assert data["method"] == "GET"
@@ -48,6 +52,7 @@ def test_middleware_should_call_apilytics_api(
4852
assert data["responseSize"] > 0
4953
assert isinstance(data["timeMillis"], int)
5054
if platform.system() == "Linux":
55+
assert isinstance(data["cpuUsage"], float)
5156
assert isinstance(data["memoryUsage"], int)
5257
assert isinstance(data["memoryTotal"], int)
5358

@@ -128,14 +133,19 @@ def test_middleware_should_work_with_streaming_response(
128133
"statusCode",
129134
"requestSize",
130135
"timeMillis",
131-
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
136+
*(
137+
("cpuUsage", "memoryUsage", "memoryTotal")
138+
if platform.system() == "Linux"
139+
else ()
140+
),
132141
}
133142
assert data["path"] == "/streaming"
134143
assert data["method"] == "GET"
135144
assert data["statusCode"] == 200
136145
assert data["requestSize"] == 0
137146
assert isinstance(data["timeMillis"], int)
138147
if platform.system() == "Linux":
148+
assert isinstance(data["cpuUsage"], float)
139149
assert isinstance(data["memoryUsage"], int)
140150
assert isinstance(data["memoryTotal"], int)
141151

@@ -169,7 +179,11 @@ def test_middleware_should_send_data_even_on_errors(
169179
"statusCode",
170180
"requestSize",
171181
"responseSize",
172-
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
182+
*(
183+
("cpuUsage", "memoryUsage", "memoryTotal")
184+
if platform.system() == "Linux"
185+
else ()
186+
),
173187
}
174188
assert data["method"] == "GET"
175189
assert data["path"] == "/error"
@@ -178,5 +192,6 @@ def test_middleware_should_send_data_even_on_errors(
178192
assert data["responseSize"] > 0
179193
assert isinstance(data["timeMillis"], int)
180194
if platform.system() == "Linux":
195+
assert isinstance(data["cpuUsage"], float)
181196
assert isinstance(data["memoryUsage"], int)
182197
assert isinstance(data["memoryTotal"], int)

tests/fastapi/test_fastapi.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ def test_middleware_should_call_apilytics_api(
4545
"responseSize",
4646
"userAgent",
4747
"timeMillis",
48-
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
48+
*(
49+
("cpuUsage", "memoryUsage", "memoryTotal")
50+
if platform.system() == "Linux"
51+
else ()
52+
),
4953
}
5054
assert data["path"] == "/"
5155
assert data["method"] == "GET"
@@ -55,6 +59,7 @@ def test_middleware_should_call_apilytics_api(
5559
assert data["userAgent"] == "testclient"
5660
assert isinstance(data["timeMillis"], int)
5761
if platform.system() == "Linux":
62+
assert isinstance(data["cpuUsage"], float)
5863
assert isinstance(data["memoryUsage"], int)
5964
assert isinstance(data["memoryTotal"], int)
6065

@@ -133,7 +138,11 @@ def test_middleware_should_work_with_streaming_response(
133138
"requestSize",
134139
"userAgent",
135140
"timeMillis",
136-
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
141+
*(
142+
("cpuUsage", "memoryUsage", "memoryTotal")
143+
if platform.system() == "Linux"
144+
else ()
145+
),
137146
}
138147
assert data["path"] == "/streaming"
139148
assert data["method"] == "GET"
@@ -142,6 +151,7 @@ def test_middleware_should_work_with_streaming_response(
142151
assert data["userAgent"] == "testclient"
143152
assert isinstance(data["timeMillis"], int)
144153
if platform.system() == "Linux":
154+
assert isinstance(data["cpuUsage"], float)
145155
assert isinstance(data["memoryUsage"], int)
146156
assert isinstance(data["memoryTotal"], int)
147157

@@ -181,13 +191,18 @@ def test_middleware_should_send_data_even_on_errors(
181191
"timeMillis",
182192
"userAgent",
183193
"requestSize",
184-
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
194+
*(
195+
("cpuUsage", "memoryUsage", "memoryTotal")
196+
if platform.system() == "Linux"
197+
else ()
198+
),
185199
}
186200
assert data["method"] == "GET"
187201
assert data["path"] == "/error"
188202
assert data["requestSize"] == 0
189203
assert data["userAgent"] == "testclient"
190204
assert isinstance(data["timeMillis"], int)
191205
if platform.system() == "Linux":
206+
assert isinstance(data["cpuUsage"], float)
192207
assert isinstance(data["memoryUsage"], int)
193208
assert isinstance(data["memoryTotal"], int)

0 commit comments

Comments
 (0)