Skip to content
This repository was archived by the owner on Sep 2, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

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

Expand Down
55 changes: 55 additions & 0 deletions apilytics/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def set_response_info(

def _send_metrics(self) -> None:
memory_usage, memory_total = _get_used_and_total_memory()
cpu_usage = _get_cpu_usage()

request = urllib.request.Request(
url="https://www.apilytics.io/api/v1/middleware",
Expand Down Expand Up @@ -161,6 +162,7 @@ def _send_metrics(self) -> None:
if self._response_size is not None
else {}
),
**({"cpuUsage": cpu_usage} if cpu_usage is not None else {}),
**({"memoryUsage": memory_usage} if memory_usage is not None else {}),
**({"memoryTotal": memory_total} if memory_total is not None else {}),
}
Expand All @@ -170,6 +172,59 @@ def _send_metrics(self) -> None:
pass


def _get_cpu_usage() -> Optional[float]:
"""
Get the current CPU usage as a percentage.

Returns:
A percentage value between 0 and 1, or None if the CPU usage could not be
determined (most likely because the system is not Linux).
"""
if platform.system() != "Linux":
return None

def cpu_times() -> Tuple[int, int]:
with open("/proc/stat") as f:
stat = f.readline()

# Ignore the `cpu` text from the start and the last two "guest" times.
times = [int(val) for val in stat.split()[1:9]]

total = sum(times)
idle = times[3]

try:
# Include `iowait` time into idle time if available, as does:
# https://github.com/torvalds/linux/blob/4f12b742eb2b3a850ac8be7dc4ed52976fc6cb0b/kernel/sched/cputime.c#L225
idle += times[4]
except IndexError:
# `iowait` time is not available before Linux 2.5.41, quite unlikely
# to happen but doesn't hurt to handle this anyway.
pass

return idle, total

try:
idle_start, total_start = cpu_times()

# There is no such thing as CPU usage percentage on a single point of time.
# At any discrete instant a CPU core is either fully used or fully idle.
# This is why we need to measure the usage over a known time interval. An
# interval of one second has been tested to provide quite consistent results.
time.sleep(1)

idle_end, total_end = cpu_times()
except OSError:
return None

try:
idle_percentage = (idle_end - idle_start) / (total_end - total_start)
except ZeroDivisionError:
return 0.0

return 1 - idle_percentage


def _get_used_and_total_memory() -> Tuple[Optional[int], Optional[int]]:
"""
Get information about the used and total system memory.
Expand Down
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ def mocked_executor() -> Generator[None, None, None]:
new=_MockedExecutor,
):
yield


@pytest.fixture(scope="session", autouse=True)
def mocked_sleep() -> Generator[None, None, None]:
with unittest.mock.patch("apilytics.core.time.sleep", new=lambda secs: None):
yield
21 changes: 18 additions & 3 deletions tests/django/test_django.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ def test_middleware_should_call_apilytics_api(
"requestSize",
"responseSize",
"timeMillis",
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
*(
("cpuUsage", "memoryUsage", "memoryTotal")
if platform.system() == "Linux"
else ()
),
}
assert data["path"] == "/"
assert data["method"] == "GET"
Expand All @@ -48,6 +52,7 @@ def test_middleware_should_call_apilytics_api(
assert data["responseSize"] > 0
assert isinstance(data["timeMillis"], int)
if platform.system() == "Linux":
assert isinstance(data["cpuUsage"], float)
assert isinstance(data["memoryUsage"], int)
assert isinstance(data["memoryTotal"], int)

Expand Down Expand Up @@ -128,14 +133,19 @@ def test_middleware_should_work_with_streaming_response(
"statusCode",
"requestSize",
"timeMillis",
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
*(
("cpuUsage", "memoryUsage", "memoryTotal")
if platform.system() == "Linux"
else ()
),
}
assert data["path"] == "/streaming"
assert data["method"] == "GET"
assert data["statusCode"] == 200
assert data["requestSize"] == 0
assert isinstance(data["timeMillis"], int)
if platform.system() == "Linux":
assert isinstance(data["cpuUsage"], float)
assert isinstance(data["memoryUsage"], int)
assert isinstance(data["memoryTotal"], int)

Expand Down Expand Up @@ -169,7 +179,11 @@ def test_middleware_should_send_data_even_on_errors(
"statusCode",
"requestSize",
"responseSize",
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
*(
("cpuUsage", "memoryUsage", "memoryTotal")
if platform.system() == "Linux"
else ()
),
}
assert data["method"] == "GET"
assert data["path"] == "/error"
Expand All @@ -178,5 +192,6 @@ def test_middleware_should_send_data_even_on_errors(
assert data["responseSize"] > 0
assert isinstance(data["timeMillis"], int)
if platform.system() == "Linux":
assert isinstance(data["cpuUsage"], float)
assert isinstance(data["memoryUsage"], int)
assert isinstance(data["memoryTotal"], int)
21 changes: 18 additions & 3 deletions tests/fastapi/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ def test_middleware_should_call_apilytics_api(
"responseSize",
"userAgent",
"timeMillis",
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
*(
("cpuUsage", "memoryUsage", "memoryTotal")
if platform.system() == "Linux"
else ()
),
}
assert data["path"] == "/"
assert data["method"] == "GET"
Expand All @@ -55,6 +59,7 @@ def test_middleware_should_call_apilytics_api(
assert data["userAgent"] == "testclient"
assert isinstance(data["timeMillis"], int)
if platform.system() == "Linux":
assert isinstance(data["cpuUsage"], float)
assert isinstance(data["memoryUsage"], int)
assert isinstance(data["memoryTotal"], int)

Expand Down Expand Up @@ -133,7 +138,11 @@ def test_middleware_should_work_with_streaming_response(
"requestSize",
"userAgent",
"timeMillis",
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
*(
("cpuUsage", "memoryUsage", "memoryTotal")
if platform.system() == "Linux"
else ()
),
}
assert data["path"] == "/streaming"
assert data["method"] == "GET"
Expand All @@ -142,6 +151,7 @@ def test_middleware_should_work_with_streaming_response(
assert data["userAgent"] == "testclient"
assert isinstance(data["timeMillis"], int)
if platform.system() == "Linux":
assert isinstance(data["cpuUsage"], float)
assert isinstance(data["memoryUsage"], int)
assert isinstance(data["memoryTotal"], int)

Expand Down Expand Up @@ -181,13 +191,18 @@ def test_middleware_should_send_data_even_on_errors(
"timeMillis",
"userAgent",
"requestSize",
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
*(
("cpuUsage", "memoryUsage", "memoryTotal")
if platform.system() == "Linux"
else ()
),
}
assert data["method"] == "GET"
assert data["path"] == "/error"
assert data["requestSize"] == 0
assert data["userAgent"] == "testclient"
assert isinstance(data["timeMillis"], int)
if platform.system() == "Linux":
assert isinstance(data["cpuUsage"], float)
assert isinstance(data["memoryUsage"], int)
assert isinstance(data["memoryTotal"], int)
Loading