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

1import threading 

2from datetime import timedelta 

3 

4from prometheus_client import Counter, Gauge, Histogram, exposition 

5from prometheus_client.registry import CollectorRegistry 

6from sqlalchemy.sql import func 

7 

8from couchers.db import session_scope 

9from couchers.models import EventOccurrenceAttendee, HostingStatus, HostRequest, Message, Reference, User 

10from couchers.sql import couchers_select as select 

11 

12main_process_registry = CollectorRegistry() 

13job_process_registry = CollectorRegistry() 

14 

15_INF = float("inf") 

16 

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) 

23 

24 

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) 

27 

28 

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) 

35 

36 

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) 

39 

40 

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 

44 

45 statement should be a sqlalchemy SELECT statement that returns a single number 

46 """ 

47 

48 def func(): 

49 with session_scope() as session: 

50 return session.execute(statement).scalar_one() 

51 

52 gauge = Gauge( 

53 name, 

54 description, 

55 registry=main_process_registry, 

56 ) 

57 gauge.set_function(func) 

58 return gauge 

59 

60 

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] 

76 

77users_gauge = _make_gauge_from_query( 

78 "couchers_users", "Total number of users", select(func.count()).select_from(User).where(User.is_visible) 

79) 

80 

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) 

86 

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) 

92 

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) 

98 

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) 

104 

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) 

110 

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) 

116 

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) 

122 

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) 

136 

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) 

150 

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) 

164 

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) 

178 

179 

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) 

198 

199logins_counter = Counter( 

200 "couchers_logins_total", 

201 "Number of logins", 

202 labelnames=["gender"], 

203 registry=main_process_registry, 

204) 

205 

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) 

216 

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) 

235 

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) 

253 

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) 

266 

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) 

273 

274 

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) 

337 

338 

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