Coverage for src/couchers/metrics.py: 97%
61 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-20 18:03 +0000
1import threading
2from datetime import timedelta
4from prometheus_client import Counter, Gauge, Histogram, exposition
5from prometheus_client.registry import CollectorRegistry
6from sqlalchemy.sql import func
8from couchers.db import session_scope
9from couchers.models import EventOccurrenceAttendee, HostingStatus, HostRequest, Message, Reference, User
10from couchers.sql import couchers_select as select
12main_process_registry = CollectorRegistry()
13job_process_registry = CollectorRegistry()
15_INF = float("inf")
17jobs_duration_histogram = Histogram(
18 "couchers_background_jobs_seconds",
19 "Durations of background jobs",
20 labelnames=["job", "status", "attempt", "exception"],
21 registry=job_process_registry,
22)
25def observe_in_jobs_duration_histogram(job_type, job_state, try_count, exception_name, duration_s):
26 jobs_duration_histogram.labels(job_type, job_state, str(try_count), exception_name).observe(duration_s)
29servicer_duration_histogram = Histogram(
30 "couchers_servicer_duration_seconds",
31 "Durations of processing gRPC calls",
32 labelnames=["method", "logged_in", "code", "exception"],
33 registry=main_process_registry,
34)
37def observe_in_servicer_duration_histogram(method, user_id, status_code, exception_type, duration_s):
38 servicer_duration_histogram.labels(method, user_id is not None, status_code, exception_type).observe(duration_s)
41def _make_gauge_from_query(name, description, statement):
42 """
43 Given a name, description and statement that is a sqlalchemy statement, creates a gauge from it
45 statement should be a sqlalchemy SELECT statement that returns a single number
46 """
48 def func():
49 with session_scope() as session:
50 return session.execute(statement).scalar_one()
52 gauge = Gauge(
53 name,
54 description,
55 registry=main_process_registry,
56 )
57 gauge.set_function(func)
58 return gauge
61active_users_gauges = [
62 _make_gauge_from_query(
63 f"couchers_active_users_{name}",
64 f"Number of active users in the last {description}",
65 (select(func.count()).select_from(User).where(User.is_visible).where(User.last_active > func.now() - interval)),
66 )
67 for name, description, interval in [
68 ("5m", "5 min", timedelta(minutes=5)),
69 ("24h", "24 hours", timedelta(hours=24)),
70 ("1month", "1 month", timedelta(days=31)),
71 ("3month", "3 months", timedelta(days=92)),
72 ("6month", "6 months", timedelta(days=183)),
73 ("12month", "12 months", timedelta(days=365)),
74 ]
75]
77users_gauge = _make_gauge_from_query(
78 "couchers_users", "Total number of users", select(func.count()).select_from(User).where(User.is_visible)
79)
81man_gauge = _make_gauge_from_query(
82 "couchers_users_man",
83 "Total number of users with gender 'Man'",
84 select(func.count()).select_from(User).where(User.is_visible).where(User.gender == "Man"),
85)
87woman_gauge = _make_gauge_from_query(
88 "couchers_users_woman",
89 "Total number of users with gender 'Woman'",
90 select(func.count()).select_from(User).where(User.is_visible).where(User.gender == "Woman"),
91)
93nonbinary_gauge = _make_gauge_from_query(
94 "couchers_users_nonbinary",
95 "Total number of users with gender 'Non-binary'",
96 select(func.count()).select_from(User).where(User.is_visible).where(User.gender == "Non-binary"),
97)
99can_host_gauge = _make_gauge_from_query(
100 "couchers_users_can_host",
101 "Total number of users with hosting status 'can_host'",
102 select(func.count()).select_from(User).where(User.is_visible).where(User.hosting_status == HostingStatus.can_host),
103)
105cant_host_gauge = _make_gauge_from_query(
106 "couchers_users_cant_host",
107 "Total number of users with hosting status 'cant_host'",
108 select(func.count()).select_from(User).where(User.is_visible).where(User.hosting_status == HostingStatus.cant_host),
109)
111maybe_gauge = _make_gauge_from_query(
112 "couchers_users_maybe",
113 "Total number of users with hosting status 'maybe'",
114 select(func.count()).select_from(User).where(User.is_visible).where(User.hosting_status == HostingStatus.maybe),
115)
117completed_profile_gauge = _make_gauge_from_query(
118 "couchers_users_completed_profile",
119 "Total number of users with a completed profile",
120 select(func.count()).select_from(User).where(User.is_visible).where(User.has_completed_profile),
121)
123sent_message_gauge = _make_gauge_from_query(
124 "couchers_users_sent_message",
125 "Total number of users who have sent a message",
126 (
127 select(func.count()).select_from(
128 select(User.id)
129 .where(User.is_visible)
130 .join(Message, Message.author_id == User.id)
131 .group_by(User.id)
132 .subquery()
133 )
134 ),
135)
137sent_request_gauge = _make_gauge_from_query(
138 "couchers_users_sent_request",
139 "Total number of users who have sent a host request",
140 (
141 select(func.count()).select_from(
142 select(User.id)
143 .where(User.is_visible)
144 .join(HostRequest, HostRequest.surfer_user_id == User.id)
145 .group_by(User.id)
146 .subquery()
147 )
148 ),
149)
151has_reference_gauge = _make_gauge_from_query(
152 "couchers_users_has_reference",
153 "Total number of users who have a reference",
154 (
155 select(func.count()).select_from(
156 select(User.id)
157 .where(User.is_visible)
158 .join(Reference, Reference.to_user_id == User.id)
159 .group_by(User.id)
160 .subquery()
161 )
162 ),
163)
165rsvpd_to_event_gauge = _make_gauge_from_query(
166 "couchers_users_rsvpd_to_event",
167 "Total number of users who have RSVPd to an event",
168 (
169 select(func.count()).select_from(
170 select(User.id)
171 .where(User.is_visible)
172 .join(EventOccurrenceAttendee, EventOccurrenceAttendee.user_id == User.id)
173 .group_by(User.id)
174 .subquery()
175 )
176 ),
177)
180signup_initiations_counter = Counter(
181 "couchers_signup_initiations_total",
182 "Number of initiated signups",
183 registry=main_process_registry,
184)
185signup_completions_counter = Counter(
186 "couchers_signup_completions_total",
187 "Number of completed signups",
188 labelnames=["gender"],
189 registry=main_process_registry,
190)
191signup_time_histogram = Histogram(
192 "couchers_signup_time_seconds",
193 "Time taken for a user to sign up",
194 labelnames=["gender"],
195 registry=main_process_registry,
196 buckets=(30, 60, 90, 120, 180, 240, 300, 360, 420, 480, 540, 600, 900, 1200, 1800, 3600, 7200, _INF),
197)
199logins_counter = Counter(
200 "couchers_logins_total",
201 "Number of logins",
202 labelnames=["gender"],
203 registry=main_process_registry,
204)
206password_reset_initiations_counter = Counter(
207 "couchers_password_reset_initiations_total",
208 "Number of password reset initiations",
209 registry=main_process_registry,
210)
211password_reset_completions_counter = Counter(
212 "couchers_password_reset_completions_total",
213 "Number of password reset completions",
214 registry=main_process_registry,
215)
217account_deletion_initiations_counter = Counter(
218 "couchers_account_deletion_initiations_total",
219 "Number of account deletion initiations",
220 labelnames=["gender"],
221 registry=main_process_registry,
222)
223account_deletion_completions_counter = Counter(
224 "couchers_account_deletion_completions_total",
225 "Number of account deletion completions",
226 labelnames=["gender"],
227 registry=main_process_registry,
228)
229account_recoveries_counter = Counter(
230 "couchers_account_recoveries_total",
231 "Number of account recoveries",
232 labelnames=["gender"],
233 registry=main_process_registry,
234)
236strong_verification_initiations_counter = Counter(
237 "couchers_strong_verification_initiations_total",
238 "Number of strong verification initiations",
239 labelnames=["gender"],
240 registry=main_process_registry,
241)
242strong_verification_completions_counter = Counter(
243 "couchers_strong_verification_completions_total",
244 "Number of strong verification completions",
245 registry=main_process_registry,
246)
247strong_verification_data_deletions_counter = Counter(
248 "couchers_strong_verification_data_deletions_total",
249 "Number of strong verification data deletions",
250 labelnames=["gender"],
251 registry=main_process_registry,
252)
254host_requests_sent_counter = Counter(
255 "couchers_host_requests_total",
256 "Number of host requests sent",
257 labelnames=["from_gender", "to_gender"],
258 registry=main_process_registry,
259)
260host_request_responses_counter = Counter(
261 "couchers_host_requests_responses_total",
262 "Number of responses to host requests",
263 labelnames=["responder_gender", "other_gender", "response_type"],
264 registry=main_process_registry,
265)
267sent_messages_counter = Counter(
268 "couchers_sent_messages_total",
269 "Number of messages sent",
270 labelnames=["gender", "message_type"],
271 registry=main_process_registry,
272)
275host_request_first_response_histogram = Histogram(
276 "couchers_host_request_first_response_seconds",
277 "Response time to host requests",
278 labelnames=["host_gender", "surfer_gender", "response_type"],
279 registry=main_process_registry,
280 buckets=(
281 1 * 60, # 1m
282 2 * 60, # 2m
283 5 * 60, # 5m
284 10 * 60, # 10m
285 15 * 60, # 15m
286 30 * 60, # 30m
287 45 * 60, # 45m
288 3_600, # 1h
289 2 * 3_600, # 2h
290 3 * 3_600, # 3h
291 6 * 3_600, # 6h
292 12 * 3_600, # 12h
293 86_400, # 24h
294 2 * 86_400, # 2d
295 5 * 86_400, # 4d
296 602_000, # 1w
297 2 * 602_000, # 2w
298 3 * 602_000, # 3w
299 4 * 602_000, # 4w
300 _INF,
301 ),
302)
303account_age_on_host_request_create_histogram = Histogram(
304 "couchers_account_age_on_host_request_create_histogram_seconds",
305 "Age of account sending a host request",
306 labelnames=["surfer_gender", "host_gender"],
307 registry=main_process_registry,
308 buckets=(
309 5 * 60, # 5m
310 10 * 60, # 10m
311 15 * 60, # 15m
312 30 * 60, # 30m
313 45 * 60, # 45m
314 3_600, # 1h
315 2 * 3_600, # 2h
316 3 * 3_600, # 3h
317 6 * 3_600, # 6h
318 12 * 3_600, # 12h
319 86_400, # 24h
320 2 * 86_400, # 2d
321 3 * 86_400, # 3d
322 4 * 86_400, # 4d
323 5 * 86_400, # 5d
324 6 * 86_400, # 6d
325 602_000, # 1w
326 2 * 602_000, # 2w
327 3 * 602_000, # 3w
328 4 * 602_000, # 4w
329 5 * 602_000, # 5w
330 10 * 602_000, # 10w
331 25 * 602_000, # 25w
332 52 * 602_000, # 52w
333 104 * 602_000, # 104w
334 _INF,
335 ),
336)
339def create_prometheus_server(registry, port):
340 """custom start method to fix problem descrbied in https://github.com/prometheus/client_python/issues/155"""
341 app = exposition.make_wsgi_app(registry)
342 httpd = exposition.make_server(
343 "", port, app, exposition.ThreadingWSGIServer, handler_class=exposition._SilentHandler
344 )
345 t = threading.Thread(target=httpd.serve_forever)
346 t.daemon = True
347 t.start()
348 return httpd