1mod fmt;
8mod future;
9mod layer;
10mod service;
11
12use std::{
13 borrow::Cow,
14 sync::{
15 Arc, OnceLock,
16 atomic::{AtomicU64, Ordering},
17 },
18 time::Duration,
19};
20
21use quanta::Instant;
22use tokio::task_local;
23use ulid::Ulid;
24
25pub use self::{
26 fmt::EventFormatter,
27 future::{LogContextFuture, PollRecordingFuture},
28 layer::LogContextLayer,
29 service::LogContextService,
30};
31
32#[derive(Clone, Debug)]
39pub enum Requester {
40 User {
42 id: Ulid,
43 username: Option<String>,
46 },
47
48 OAuth2Client { id: Ulid },
51
52 Homeserver,
55}
56
57impl Requester {
58 #[must_use]
60 pub fn user(id: Ulid, username: impl Into<String>) -> Self {
61 Self::User {
62 id,
63 username: Some(username.into()),
64 }
65 }
66
67 #[must_use]
69 pub fn user_id_only(id: Ulid) -> Self {
70 Self::User { id, username: None }
71 }
72
73 #[must_use]
75 pub fn oauth2_client(id: Ulid) -> Self {
76 Self::OAuth2Client { id }
77 }
78}
79
80impl std::fmt::Display for Requester {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::User {
84 id,
85 username: Some(name),
86 } => write!(f, "user:{id}({name})"),
87 Self::User { id, username: None } => write!(f, "user:{id}"),
88 Self::OAuth2Client { id } => write!(f, "oauth2-client:{id}"),
89 Self::Homeserver => write!(f, "homeserver"),
90 }
91 }
92}
93
94static LOG_CONTEXT_INDEX: AtomicU64 = AtomicU64::new(0);
97task_local! {
98 pub static CURRENT_LOG_CONTEXT: LogContext;
99}
100
101#[derive(Clone)]
104pub struct LogContext {
105 inner: Arc<LogContextInner>,
106}
107
108struct LogContextInner {
109 tag: Cow<'static, str>,
111
112 index: u64,
114
115 start: Instant,
117
118 polls: AtomicU64,
120
121 cpu_time: AtomicU64,
124
125 db_queries: AtomicU64,
127
128 db_time: AtomicU64,
131
132 db_rows_fetched: AtomicU64,
134
135 requester: OnceLock<Requester>,
138
139 hub: Arc<sentry::Hub>,
144}
145
146impl LogContext {
147 pub fn new(tag: impl Into<Cow<'static, str>>) -> Self {
149 let tag = tag.into();
150 let inner = LogContextInner {
151 tag,
152 index: LOG_CONTEXT_INDEX.fetch_add(1, Ordering::Relaxed),
153 start: Instant::now(),
154 polls: AtomicU64::new(0),
155 cpu_time: AtomicU64::new(0),
156 db_queries: AtomicU64::new(0),
157 db_rows_fetched: AtomicU64::new(0),
158 db_time: AtomicU64::new(0),
159 requester: OnceLock::new(),
160 hub: Arc::new(sentry::Hub::new(
161 sentry::Hub::main().client(),
162 Arc::default(),
163 )),
164 };
165
166 Self {
167 inner: Arc::new(inner),
168 }
169 }
170
171 #[must_use]
173 pub fn tag(&self) -> &Cow<'static, str> {
174 &self.inner.tag
175 }
176
177 pub fn maybe_with<F, R>(f: F) -> Option<R>
179 where
180 F: FnOnce(&Self) -> R,
181 {
182 CURRENT_LOG_CONTEXT.try_with(f).ok()
183 }
184
185 pub fn run<F: FnOnce() -> Fut, Fut: Future>(&self, f: F) -> LogContextFuture<Fut> {
188 let future = self.run_sync(f);
189 self.wrap_future(future)
190 }
191
192 pub fn run_sync<F: FnOnce() -> R, R>(&self, f: F) -> R {
195 let start = Instant::now();
196 let result = sentry::Hub::run(self.inner.hub.clone(), || {
197 CURRENT_LOG_CONTEXT.sync_scope(self.clone(), f)
198 });
199 let elapsed = start.elapsed().as_nanos().try_into().unwrap_or(u64::MAX);
200 self.inner.cpu_time.fetch_add(elapsed, Ordering::Relaxed);
201 result
202 }
203
204 pub fn maybe_record_query_stats(fetched: usize, duration: Duration) {
206 LogContext::maybe_with(|ctx| ctx.record_query_stats(fetched, duration));
207 }
208
209 pub fn record_query_stats(&self, fetched: usize, duration: Duration) {
211 let nanos = duration.as_nanos().try_into().unwrap_or(u64::MAX);
212 self.inner.db_time.fetch_add(nanos, Ordering::Relaxed);
213 self.inner
214 .db_rows_fetched
215 .fetch_add(fetched as u64, Ordering::Relaxed);
216 self.inner.db_queries.fetch_add(1, Ordering::Relaxed);
217 }
218
219 pub fn set_requester(&self, requester: Requester) {
222 let _ = self.inner.requester.set(requester);
223 }
224
225 #[must_use]
227 pub fn requester(&self) -> Option<&Requester> {
228 self.inner.requester.get()
229 }
230
231 pub fn maybe_set_requester(requester: Requester) {
234 Self::maybe_with(|ctx| ctx.set_requester(requester));
235 }
236
237 #[must_use]
239 pub fn stats(&self) -> LogContextStats {
240 let polls = self.inner.polls.load(Ordering::Relaxed);
241 let cpu_time = self.inner.cpu_time.load(Ordering::Relaxed);
242 let cpu_time = Duration::from_nanos(cpu_time);
243 let elapsed = self.inner.start.elapsed();
244 let db_queries = self.inner.db_queries.load(Ordering::Relaxed);
245 let db_rows_fetched = self.inner.db_rows_fetched.load(Ordering::Relaxed);
246 let db_time = Duration::from_nanos(self.inner.db_time.load(Ordering::Relaxed));
247 LogContextStats {
248 polls,
249 cpu_time,
250 elapsed,
251 db_queries,
252 db_rows_fetched,
253 db_time,
254 }
255 }
256}
257
258impl std::fmt::Display for LogContext {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 let tag = &self.inner.tag;
261 let index = self.inner.index;
262 write!(f, "{tag}-{index}")
263 }
264}
265
266#[derive(Debug, Clone, Copy)]
268pub struct LogContextStats {
269 pub polls: u64,
271
272 pub cpu_time: Duration,
274
275 pub elapsed: Duration,
277
278 pub db_queries: u64,
280
281 pub db_rows_fetched: u64,
283
284 pub db_time: Duration,
286}
287
288impl std::fmt::Display for LogContextStats {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 let polls = self.polls;
291 #[expect(clippy::cast_precision_loss)]
292 let cpu_time_ms = self.cpu_time.as_nanos() as f64 / 1_000_000.;
293 #[expect(clippy::cast_precision_loss)]
294 let elapsed_ms = self.elapsed.as_nanos() as f64 / 1_000_000.;
295 let db_queries = self.db_queries;
296 let db_rows_fetched = self.db_rows_fetched;
297 #[expect(clippy::cast_precision_loss)]
298 let db_time_ms = self.db_time.as_nanos() as f64 / 1_000_000.;
299 write!(
300 f,
301 "polls: {polls}, cpu: {cpu_time_ms:.1}ms, db: {db_time_ms:.1}ms, elapsed: {elapsed_ms:.1}ms, queries: {db_queries}, fetched: {db_rows_fetched}",
302 )
303 }
304}