Skip to content

Commit 8326675

Browse files
feat(beacon): Add cpu/ram usage to beacon (#65200)
This adds CPU/RAM usage to the beacon. It also adds `psutil` as a dependency --------- Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
1 parent 9af303c commit 8326675

9 files changed

+167
-9
lines changed

requirements-base.txt

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ phonenumberslite>=8.12.32
4242
Pillow>=10.2.0
4343
progressbar2>=3.41.0
4444
python-rapidjson>=1.4
45+
psutil>=5.9.2
4546
psycopg2-binary>=2.9.9
4647
PyJWT>=2.4.0
4748
pydantic>=1.10.9

requirements-dev-frozen.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ progressbar2==3.41.0
127127
prompt-toolkit==3.0.41
128128
proto-plus==1.23.0
129129
protobuf==4.25.2
130-
psutil==5.9.2
130+
psutil==5.9.7
131131
psycopg2-binary==2.9.9
132132
pyasn1==0.4.5
133133
pyasn1-modules==0.2.4

requirements-dev.txt

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ docker>=6
77
time-machine>=2.13.0
88
honcho>=1.1.0
99
openapi-core>=0.18.2
10-
psutil
1110
pytest>=8
1211
pytest-cov>=4.0.0
1312
pytest-django>=4.8.0

requirements-frozen.txt

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ progressbar2==3.41.0
8686
prompt-toolkit==3.0.41
8787
proto-plus==1.23.0
8888
protobuf==4.25.2
89+
psutil==5.9.7
8990
psycopg2-binary==2.9.9
9091
pyasn1==0.4.5
9192
pyasn1-modules==0.2.4

src/sentry/options/defaults.py

+5
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,11 @@
285285

286286
# Beacon
287287
register("beacon.anonymous", type=Bool, flags=FLAG_REQUIRED)
288+
register(
289+
"beacon.record_cpu_ram_usage",
290+
type=Bool,
291+
flags=FLAG_ALLOW_EMPTY | FLAG_REQUIRED,
292+
)
288293

289294
# Filestore (default)
290295
register("filestore.backend", default="filesystem", flags=FLAG_NOSTORE)

src/sentry/tasks/beacon.py

+16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from hashlib import sha1
55
from uuid import uuid4
66

7+
import psutil
78
from django.conf import settings
89
from django.utils import timezone
910

@@ -120,7 +121,14 @@ def send_beacon():
120121
# we need this to be explicitly configured and it defaults to None,
121122
# which is the same as False
122123
anonymous = options.get("beacon.anonymous") is not False
124+
# getting an option sets it to the default value, so let's avoid doing that if for some reason consent prompt is somehow skipped because of this
125+
send_cpu_ram_usage = (
126+
options.get("beacon.record_cpu_ram_usage")
127+
if options.isset("beacon.record_cpu_ram_usage")
128+
else False
129+
)
123130
event_categories_count = get_category_event_count_24h()
131+
byte_in_gibibyte = 1024**3
124132

125133
payload = {
126134
"install_id": install_id,
@@ -138,6 +146,14 @@ def send_beacon():
138146
"replays.24h": event_categories_count["replay"],
139147
"profiles.24h": event_categories_count["profile"],
140148
"monitors.24h": event_categories_count["monitor"],
149+
"cpu_cores_available": psutil.cpu_count() if send_cpu_ram_usage else None,
150+
"cpu_percentage_utilized": psutil.cpu_percent() if send_cpu_ram_usage else None,
151+
"ram_available_gb": (
152+
psutil.virtual_memory().total / byte_in_gibibyte if send_cpu_ram_usage else None
153+
),
154+
"ram_percentage_utilized": (
155+
psutil.virtual_memory().percent if send_cpu_ram_usage else None
156+
),
141157
},
142158
"packages": get_all_package_versions(),
143159
"anonymous": anonymous,

src/sentry/utils/settings.py

+8
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,11 @@ def is_self_hosted() -> bool:
33
from django.conf import settings
44

55
return settings.SENTRY_SELF_HOSTED
6+
7+
8+
def should_show_beacon_consent_prompt() -> bool:
9+
from django.conf import settings
10+
11+
from sentry import options
12+
13+
return settings.SENTRY_SELF_HOSTED and not options.isset("beacon.record_cpu_ram_usage")

src/sentry/web/client_config.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from sentry.utils.assets import get_frontend_dist_prefix
3838
from sentry.utils.email import is_smtp_enabled
3939
from sentry.utils.http import is_using_customer_domain
40-
from sentry.utils.settings import is_self_hosted
40+
from sentry.utils.settings import is_self_hosted, should_show_beacon_consent_prompt
4141
from sentry.utils.support import get_support_mail
4242

4343

@@ -361,6 +361,8 @@ def get_context(self) -> Mapping[str, Any]:
361361
# Maintain isOnPremise key for backcompat (plugins?).
362362
"isOnPremise": is_self_hosted(),
363363
"isSelfHosted": is_self_hosted(),
364+
"shouldShowBeaconConsentPrompt": not self.needs_upgrade
365+
and should_show_beacon_consent_prompt(),
364366
"invitesEnabled": settings.SENTRY_ENABLE_INVITES,
365367
"gravatarBaseUrl": settings.SENTRY_GRAVATAR_BASE_URL,
366368
"termsUrl": settings.TERMS_URL,

tests/sentry/tasks/test_beacon.py

+132-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import platform
22
from datetime import timedelta
3+
from types import SimpleNamespace
34
from unittest.mock import patch
45
from uuid import uuid4
56

@@ -18,6 +19,15 @@
1819

1920

2021
@no_silo_test
22+
@patch("psutil.cpu_count", return_value=8)
23+
@patch("psutil.cpu_percent", return_value=50)
24+
@patch(
25+
"psutil.virtual_memory",
26+
return_value=SimpleNamespace(
27+
total=34359738368,
28+
percent=50,
29+
),
30+
)
2131
class SendBeaconTest(OutcomesSnubaTest):
2232
def setUp(self):
2333
super().setUp()
@@ -87,7 +97,15 @@ def setUp(self):
8797
@patch("sentry.tasks.beacon.safe_urlopen")
8898
@patch("sentry.tasks.beacon.safe_urlread")
8999
@responses.activate
90-
def test_simple(self, safe_urlread, safe_urlopen, mock_get_all_package_versions):
100+
def test_simple(
101+
self,
102+
safe_urlread,
103+
safe_urlopen,
104+
mock_get_all_package_versions,
105+
mock_cpu_count,
106+
mock_cpu_percent,
107+
mock_virtual_memory,
108+
):
91109
self.organization
92110
self.project
93111
self.team
@@ -96,6 +114,7 @@ def test_simple(self, safe_urlread, safe_urlopen, mock_get_all_package_versions)
96114

97115
assert options.set("system.admin-email", "[email protected]")
98116
assert options.set("beacon.anonymous", False)
117+
assert options.set("beacon.record_cpu_ram_usage", True)
99118
send_beacon()
100119

101120
install_id = options.get("sentry:install-id")
@@ -119,6 +138,10 @@ def test_simple(self, safe_urlread, safe_urlopen, mock_get_all_package_versions)
119138
"replays.24h": 1,
120139
"profiles.24h": 3,
121140
"monitors.24h": 0,
141+
"cpu_cores_available": 8,
142+
"cpu_percentage_utilized": 50,
143+
"ram_available_gb": 32,
144+
"ram_percentage_utilized": 50,
122145
},
123146
"anonymous": False,
124147
"admin_email": "[email protected]",
@@ -134,7 +157,75 @@ def test_simple(self, safe_urlread, safe_urlopen, mock_get_all_package_versions)
134157
@patch("sentry.tasks.beacon.safe_urlopen")
135158
@patch("sentry.tasks.beacon.safe_urlread")
136159
@responses.activate
137-
def test_anonymous(self, safe_urlread, safe_urlopen, mock_get_all_package_versions):
160+
def test_no_cpu_ram_usage(
161+
self,
162+
safe_urlread,
163+
safe_urlopen,
164+
mock_get_all_package_versions,
165+
mock_cpu_count,
166+
mock_cpu_percent,
167+
mock_virtual_memory,
168+
):
169+
self.organization
170+
self.project
171+
self.team
172+
mock_get_all_package_versions.return_value = {"foo": "1.0"}
173+
safe_urlread.return_value = json.dumps({"notices": [], "version": {"stable": "1.0.0"}})
174+
175+
assert options.set("system.admin-email", "[email protected]")
176+
assert options.set("beacon.anonymous", False)
177+
assert options.set("beacon.record_cpu_ram_usage", False)
178+
send_beacon()
179+
180+
install_id = options.get("sentry:install-id")
181+
assert install_id and len(install_id) == 40
182+
183+
safe_urlopen.assert_called_once_with(
184+
BEACON_URL,
185+
json={
186+
"install_id": install_id,
187+
"version": sentry.get_version(),
188+
"docker": sentry.is_docker(),
189+
"python_version": platform.python_version(),
190+
"data": {
191+
"organizations": 2,
192+
"users": 1,
193+
"projects": 2,
194+
"teams": 2,
195+
"events.24h": 8, # We expect the number of events to be the sum of events from two orgs. First org has 5 events while the second org has 3 events.
196+
"errors.24h": 8,
197+
"transactions.24h": 2,
198+
"replays.24h": 1,
199+
"profiles.24h": 3,
200+
"monitors.24h": 0,
201+
"cpu_cores_available": None,
202+
"cpu_percentage_utilized": None,
203+
"ram_available_gb": None,
204+
"ram_percentage_utilized": None,
205+
},
206+
"anonymous": False,
207+
"admin_email": "[email protected]",
208+
"packages": mock_get_all_package_versions.return_value,
209+
},
210+
timeout=5,
211+
)
212+
safe_urlread.assert_called_once_with(safe_urlopen.return_value)
213+
214+
assert options.get("sentry:latest_version") == "1.0.0"
215+
216+
@patch("sentry.tasks.beacon.get_all_package_versions")
217+
@patch("sentry.tasks.beacon.safe_urlopen")
218+
@patch("sentry.tasks.beacon.safe_urlread")
219+
@responses.activate
220+
def test_anonymous(
221+
self,
222+
safe_urlread,
223+
safe_urlopen,
224+
mock_get_all_package_versions,
225+
mock_cpu_count,
226+
mock_cpu_percent,
227+
mock_virtual_memory,
228+
):
138229
self.organization
139230
self.project
140231
self.team
@@ -143,6 +234,7 @@ def test_anonymous(self, safe_urlread, safe_urlopen, mock_get_all_package_versio
143234

144235
assert options.set("system.admin-email", "[email protected]")
145236
assert options.set("beacon.anonymous", True)
237+
assert options.set("beacon.record_cpu_ram_usage", True)
146238
send_beacon()
147239

148240
install_id = options.get("sentry:install-id")
@@ -166,6 +258,10 @@ def test_anonymous(self, safe_urlread, safe_urlopen, mock_get_all_package_versio
166258
"replays.24h": 1,
167259
"profiles.24h": 3,
168260
"monitors.24h": 0,
261+
"cpu_cores_available": 8,
262+
"cpu_percentage_utilized": 50,
263+
"ram_available_gb": 32,
264+
"ram_percentage_utilized": 50,
169265
},
170266
"anonymous": True,
171267
"packages": mock_get_all_package_versions.return_value,
@@ -180,7 +276,15 @@ def test_anonymous(self, safe_urlread, safe_urlopen, mock_get_all_package_versio
180276
@patch("sentry.tasks.beacon.safe_urlopen")
181277
@patch("sentry.tasks.beacon.safe_urlread")
182278
@responses.activate
183-
def test_with_broadcasts(self, safe_urlread, safe_urlopen, mock_get_all_package_versions):
279+
def test_with_broadcasts(
280+
self,
281+
safe_urlread,
282+
safe_urlopen,
283+
mock_get_all_package_versions,
284+
mock_cpu_count,
285+
mock_cpu_percent,
286+
mock_virtual_memory,
287+
):
184288
broadcast_id = uuid4().hex
185289
mock_get_all_package_versions.return_value = {}
186290
safe_urlread.return_value = json.dumps(
@@ -236,7 +340,15 @@ def test_with_broadcasts(self, safe_urlread, safe_urlopen, mock_get_all_package_
236340
@patch("sentry.tasks.beacon.safe_urlopen")
237341
@patch("sentry.tasks.beacon.safe_urlread")
238342
@responses.activate
239-
def test_disabled(self, safe_urlread, safe_urlopen, mock_get_all_package_versions):
343+
def test_disabled(
344+
self,
345+
safe_urlread,
346+
safe_urlopen,
347+
mock_get_all_package_versions,
348+
mock_cpu_count,
349+
mock_cpu_percent,
350+
mock_virtual_memory,
351+
):
240352
mock_get_all_package_versions.return_value = {"foo": "1.0"}
241353

242354
with self.settings(SENTRY_BEACON=False):
@@ -248,7 +360,15 @@ def test_disabled(self, safe_urlread, safe_urlopen, mock_get_all_package_version
248360
@patch("sentry.tasks.beacon.safe_urlopen")
249361
@patch("sentry.tasks.beacon.safe_urlread")
250362
@responses.activate
251-
def test_debug(self, safe_urlread, safe_urlopen, mock_get_all_package_versions):
363+
def test_debug(
364+
self,
365+
safe_urlread,
366+
safe_urlopen,
367+
mock_get_all_package_versions,
368+
mock_cpu_count,
369+
mock_cpu_percent,
370+
mock_virtual_memory,
371+
):
252372
mock_get_all_package_versions.return_value = {"foo": "1.0"}
253373

254374
with self.settings(DEBUG=True):
@@ -258,7 +378,13 @@ def test_debug(self, safe_urlread, safe_urlopen, mock_get_all_package_versions):
258378

259379
@patch("sentry.tasks.beacon.safe_urlopen")
260380
@responses.activate
261-
def test_metrics(self, safe_urlopen):
381+
def test_metrics(
382+
self,
383+
safe_urlopen,
384+
mock_cpu_count,
385+
mock_cpu_percent,
386+
mock_virtual_memory,
387+
):
262388
metrics = [
263389
{
264390
"description": "SentryApp",

0 commit comments

Comments
 (0)