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

Commit e3aeeca

Browse files
authored
Merge pull request #14 from apilytics/memory-usage
Send memory usage information together with metrics on Linux
2 parents 161e9f4 + 491681e commit e3aeeca

7 files changed

Lines changed: 227 additions & 11 deletions

File tree

.github/workflows/cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ on:
1515
jobs:
1616
release:
1717
name: "Release"
18-
if: ${{ github.ref == 'refs/heads/master' }}
18+
if: github.ref == 'refs/heads/master'
1919
runs-on: ubuntu-latest
2020
steps:
2121
- name: "Checkout code"

.github/workflows/ci.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ concurrency:
1212
jobs:
1313
commits:
1414
name: "Commits"
15-
if: ${{ github.event_name == 'pull_request' }}
15+
if: github.event_name == 'pull_request'
1616
runs-on: ubuntu-latest
1717
steps:
1818
- name: "Checkout code"
@@ -93,8 +93,11 @@ jobs:
9393
fail-fast: true
9494
matrix:
9595
python-version: ['3.7', '3.8', '3.9', '3.10']
96-
97-
runs-on: ubuntu-latest
96+
os: [ubuntu-latest, macos-latest, windows-latest]
97+
defaults:
98+
run:
99+
shell: bash
100+
runs-on: ${{ matrix.os }}
98101
steps:
99102
- name: "Checkout code"
100103
uses: actions/checkout@v2
@@ -136,7 +139,7 @@ jobs:
136139
pytest --verbose --cov=. --cov-report=xml .
137140
138141
- name: "Upload coverage"
139-
if: matrix.python-version == '3.7'
142+
if: matrix.python-version == '3.7' && matrix.os == 'ubuntu-latest'
140143
uses: codecov/codecov-action@f32b3a3741e1053eb607407145bc9619351dc93b # v2.1.0
141144
with:
142145
files: coverage.xml

CHANGELOG.md

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

88
## [Unreleased]
99

10+
### Added
11+
12+
- Send current system memory usage and total available memory together with metrics on Linux systems.
13+
1014
## [1.3.0] - 2022-02-02
1115

1216
### Added

apilytics/core.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import concurrent.futures
22
import json
33
import platform
4+
import re
45
import time
56
import types
67
import urllib.error
78
import urllib.request
8-
from typing import ClassVar, Optional, Type
9+
from typing import ClassVar, Optional, Tuple, Type
910

1011
import apilytics
1112

@@ -127,6 +128,8 @@ def set_response_info(
127128
self._response_size = response_size
128129

129130
def _send_metrics(self) -> None:
131+
memory_usage, memory_total = _get_used_and_total_memory()
132+
130133
request = urllib.request.Request(
131134
url="https://www.apilytics.io/api/v1/middleware",
132135
method="POST",
@@ -158,8 +161,42 @@ def _send_metrics(self) -> None:
158161
if self._response_size is not None
159162
else {}
160163
),
164+
**({"memoryUsage": memory_usage} if memory_usage is not None else {}),
165+
**({"memoryTotal": memory_total} if memory_total is not None else {}),
161166
}
162167
try:
163168
urllib.request.urlopen(url=request, data=json.dumps(data).encode())
164169
except urllib.error.URLError:
165170
pass
171+
172+
173+
def _get_used_and_total_memory() -> Tuple[Optional[int], Optional[int]]:
174+
"""
175+
Get information about the used and total system memory.
176+
177+
Returns:
178+
A tuple containing the used and total system memory in bytes.
179+
(None, None) if the system is not Linux or if the reading fails.
180+
(None, int) if the used memory could not be determined.
181+
"""
182+
used = None
183+
total = None
184+
185+
if platform.system() == "Linux":
186+
try:
187+
with open("/proc/meminfo") as f:
188+
meminfo = f.read()
189+
except OSError:
190+
pass # Prepare for everything and anything.
191+
else:
192+
total_match = re.search(r"MemTotal:\s*(\d+)", meminfo)
193+
available_match = re.search(r"MemAvailable:\s*(\d+)", meminfo)
194+
if total_match:
195+
total = int(total_match.group(1)) * 1024
196+
if available_match:
197+
# If MemAvailable exists MemTotal will also exist.
198+
# The reverse is not always true (MemAvailable came in Linux 3.14).
199+
available = int(available_match.group(1)) * 1024
200+
used = total - available
201+
202+
return used, total

tests/django/test_django.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ def test_middleware_should_call_apilytics_api(
3838
"requestSize",
3939
"responseSize",
4040
"timeMillis",
41+
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
4142
}
4243
assert data["path"] == "/"
4344
assert data["method"] == "GET"
4445
assert data["statusCode"] == 200
4546
assert data["requestSize"] == 0
4647
assert data["responseSize"] > 0
4748
assert isinstance(data["timeMillis"], int)
49+
if platform.system() == "Linux":
50+
assert isinstance(data["memoryUsage"], int)
51+
assert isinstance(data["memoryTotal"], int)
4852

4953

5054
def test_middleware_should_send_query_params(
@@ -123,12 +127,16 @@ def test_middleware_should_work_with_streaming_response(
123127
"statusCode",
124128
"requestSize",
125129
"timeMillis",
130+
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
126131
}
127132
assert data["path"] == "/streaming"
128133
assert data["method"] == "GET"
129134
assert data["statusCode"] == 200
130135
assert data["requestSize"] == 0
131136
assert isinstance(data["timeMillis"], int)
137+
if platform.system() == "Linux":
138+
assert isinstance(data["memoryUsage"], int)
139+
assert isinstance(data["memoryTotal"], int)
132140

133141

134142
@django.test.override_settings(APILYTICS_API_KEY=None)
@@ -160,10 +168,14 @@ def test_middleware_should_send_data_even_on_errors(
160168
"statusCode",
161169
"requestSize",
162170
"responseSize",
171+
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
163172
}
164173
assert data["method"] == "GET"
165174
assert data["path"] == "/error"
166175
assert data["statusCode"] == 500
167176
assert data["requestSize"] == 0
168177
assert data["responseSize"] > 0
169178
assert isinstance(data["timeMillis"], int)
179+
if platform.system() == "Linux":
180+
assert isinstance(data["memoryUsage"], int)
181+
assert isinstance(data["memoryTotal"], int)

tests/fastapi/test_fastapi.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def test_middleware_should_call_apilytics_api(
4444
"responseSize",
4545
"userAgent",
4646
"timeMillis",
47+
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
4748
}
4849
assert data["path"] == "/"
4950
assert data["method"] == "GET"
@@ -52,6 +53,9 @@ def test_middleware_should_call_apilytics_api(
5253
assert data["responseSize"] > 0
5354
assert data["userAgent"] == "testclient"
5455
assert isinstance(data["timeMillis"], int)
56+
if platform.system() == "Linux":
57+
assert isinstance(data["memoryUsage"], int)
58+
assert isinstance(data["memoryTotal"], int)
5559

5660

5761
def test_middleware_should_send_query_params(
@@ -128,13 +132,17 @@ def test_middleware_should_work_with_streaming_response(
128132
"requestSize",
129133
"userAgent",
130134
"timeMillis",
135+
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
131136
}
132137
assert data["path"] == "/streaming"
133138
assert data["method"] == "GET"
134139
assert data["statusCode"] == 200
135140
assert data["requestSize"] == 0
136141
assert data["userAgent"] == "testclient"
137142
assert isinstance(data["timeMillis"], int)
143+
if platform.system() == "Linux":
144+
assert isinstance(data["memoryUsage"], int)
145+
assert isinstance(data["memoryTotal"], int)
138146

139147

140148
@tests.fastapi.conftest.override_middleware(
@@ -166,9 +174,19 @@ def test_middleware_should_send_data_even_on_errors(
166174

167175
__, call_kwargs = mocked_urlopen.call_args
168176
data = tests.conftest.decode_request_data(call_kwargs["data"])
169-
assert data.keys() == {"method", "path", "timeMillis", "userAgent", "requestSize"}
177+
assert data.keys() == {
178+
"method",
179+
"path",
180+
"timeMillis",
181+
"userAgent",
182+
"requestSize",
183+
*(("memoryUsage", "memoryTotal") if platform.system() == "Linux" else ()),
184+
}
170185
assert data["method"] == "GET"
171186
assert data["path"] == "/error"
172187
assert data["requestSize"] == 0
173188
assert data["userAgent"] == "testclient"
174189
assert isinstance(data["timeMillis"], int)
190+
if platform.system() == "Linux":
191+
assert isinstance(data["memoryUsage"], int)
192+
assert isinstance(data["memoryTotal"], int)

0 commit comments

Comments
 (0)