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
140impl LogContext {
141 pub fn new(tag: impl Into<Cow<'static, str>>) -> Self {
143 let tag = tag.into();
144 let inner = LogContextInner {
145 tag,
146 index: LOG_CONTEXT_INDEX.fetch_add(1, Ordering::Relaxed),
147 start: Instant::now(),
148 polls: AtomicU64::new(0),
149 cpu_time: AtomicU64::new(0),
150 db_queries: AtomicU64::new(0),
151 db_rows_fetched: AtomicU64::new(0),
152 db_time: AtomicU64::new(0),
153 requester: OnceLock::new(),
154 };
155
156 Self {
157 inner: Arc::new(inner),
158 }
159 }
160
161 pub fn maybe_with<F, R>(f: F) -> Option<R>
163 where
164 F: FnOnce(&Self) -> R,
165 {
166 CURRENT_LOG_CONTEXT.try_with(f).ok()
167 }
168
169 pub fn run<F: FnOnce() -> Fut, Fut: Future>(&self, f: F) -> LogContextFuture<Fut> {
172 let future = self.run_sync(f);
173 self.wrap_future(future)
174 }
175
176 pub fn run_sync<F: FnOnce() -> R, R>(&self, f: F) -> R {
179 let start = Instant::now();
180 let result = CURRENT_LOG_CONTEXT.sync_scope(self.clone(), f);
181 let elapsed = start.elapsed().as_nanos().try_into().unwrap_or(u64::MAX);
182 self.inner.cpu_time.fetch_add(elapsed, Ordering::Relaxed);
183 result
184 }
185
186 pub fn maybe_record_query_stats(fetched: usize, duration: Duration) {
188 LogContext::maybe_with(|ctx| ctx.record_query_stats(fetched, duration));
189 }
190
191 pub fn record_query_stats(&self, fetched: usize, duration: Duration) {
193 let nanos = duration.as_nanos().try_into().unwrap_or(u64::MAX);
194 self.inner.db_time.fetch_add(nanos, Ordering::Relaxed);
195 self.inner
196 .db_rows_fetched
197 .fetch_add(fetched as u64, Ordering::Relaxed);
198 self.inner.db_queries.fetch_add(1, Ordering::Relaxed);
199 }
200
201 pub fn set_requester(&self, requester: Requester) {
204 let _ = self.inner.requester.set(requester);
205 }
206
207 #[must_use]
209 pub fn requester(&self) -> Option<&Requester> {
210 self.inner.requester.get()
211 }
212
213 pub fn maybe_set_requester(requester: Requester) {
216 Self::maybe_with(|ctx| ctx.set_requester(requester));
217 }
218
219 #[must_use]
221 pub fn stats(&self) -> LogContextStats {
222 let polls = self.inner.polls.load(Ordering::Relaxed);
223 let cpu_time = self.inner.cpu_time.load(Ordering::Relaxed);
224 let cpu_time = Duration::from_nanos(cpu_time);
225 let elapsed = self.inner.start.elapsed();
226 let db_queries = self.inner.db_queries.load(Ordering::Relaxed);
227 let db_rows_fetched = self.inner.db_rows_fetched.load(Ordering::Relaxed);
228 let db_time = Duration::from_nanos(self.inner.db_time.load(Ordering::Relaxed));
229 LogContextStats {
230 polls,
231 cpu_time,
232 elapsed,
233 db_queries,
234 db_rows_fetched,
235 db_time,
236 }
237 }
238}
239
240impl std::fmt::Display for LogContext {
241 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242 let tag = &self.inner.tag;
243 let index = self.inner.index;
244 write!(f, "{tag}-{index}")
245 }
246}
247
248#[derive(Debug, Clone, Copy)]
250pub struct LogContextStats {
251 pub polls: u64,
253
254 pub cpu_time: Duration,
256
257 pub elapsed: Duration,
259
260 pub db_queries: u64,
262
263 pub db_rows_fetched: u64,
265
266 pub db_time: Duration,
268}
269
270impl std::fmt::Display for LogContextStats {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 let polls = self.polls;
273 #[expect(clippy::cast_precision_loss)]
274 let cpu_time_ms = self.cpu_time.as_nanos() as f64 / 1_000_000.;
275 #[expect(clippy::cast_precision_loss)]
276 let elapsed_ms = self.elapsed.as_nanos() as f64 / 1_000_000.;
277 let db_queries = self.db_queries;
278 let db_rows_fetched = self.db_rows_fetched;
279 #[expect(clippy::cast_precision_loss)]
280 let db_time_ms = self.db_time.as_nanos() as f64 / 1_000_000.;
281 write!(
282 f,
283 "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}",
284 )
285 }
286}