1use std::sync::Arc;
13
14use axum::response::Response;
15use futures::future::BoxFuture;
16
17use crate::web::context::RequestContext;
18
19pub struct NextHandler {
22 inner: Arc<dyn Fn(RequestContext) -> BoxFuture<'static, Response> + Send + Sync>,
23}
24
25impl NextHandler {
26 #[doc(hidden)]
27 pub fn new<F>(f: F) -> Self
28 where
29 F: Fn(RequestContext) -> BoxFuture<'static, Response> + Send + Sync + 'static,
30 {
31 Self { inner: Arc::new(f) }
32 }
33
34 #[inline]
35 pub fn run(&self, ctx: RequestContext) -> BoxFuture<'static, Response> {
36 (self.inner)(ctx)
37 }
38
39 #[doc(hidden)]
42 #[inline]
43 pub fn __clone_for_chain(&self) -> Self {
44 Self {
45 inner: Arc::clone(&self.inner),
46 }
47 }
48}
49
50pub trait Interceptor: Send + Sync + 'static {
56 fn around(
57 &'static self,
58 ctx: RequestContext,
59 next: NextHandler,
60 ) -> BoxFuture<'static, Response>;
61}
62
63pub(crate) fn compose_chain(
68 globals: &'static [&'static dyn Interceptor],
69 terminal: Arc<dyn Fn(RequestContext) -> BoxFuture<'static, Response> + Send + Sync>,
70) -> Arc<dyn Fn(RequestContext) -> BoxFuture<'static, Response> + Send + Sync> {
71 let mut chain = terminal;
72 for ic in globals.iter().rev() {
73 let inner = chain;
74 let ic: &'static dyn Interceptor = *ic;
75 chain = Arc::new(move |ctx| {
76 let next = NextHandler {
77 inner: Arc::clone(&inner),
78 };
79 ic.around(ctx, next)
80 });
81 }
82 chain
83}
84
85pub struct LatencyLog;
89impl Interceptor for LatencyLog {
90 fn around(
91 &'static self,
92 ctx: RequestContext,
93 next: NextHandler,
94 ) -> BoxFuture<'static, Response> {
95 Box::pin(async move {
96 let method = ctx.method().clone();
97 let path = ctx.path().to_owned();
98 let started = std::time::Instant::now();
99 let resp = next.run(ctx).await;
100 let micros = started.elapsed().as_micros();
101 println!(
102 "[arcly] {method} {path} -> {} in {micros}\u{00B5}s",
103 resp.status().as_u16()
104 );
105 resp
106 })
107 }
108}
109
110pub struct TelemetryLog;
119
120impl Interceptor for TelemetryLog {
121 fn around(
122 &'static self,
123 ctx: RequestContext,
124 next: NextHandler,
125 ) -> BoxFuture<'static, Response> {
126 Box::pin(async move {
127 use crate::web::context::{hex_encode_16, hex_encode_8};
128 let method = ctx.method().to_string();
129 let path = ctx.path().to_owned();
130 let trace_id = hex_encode_16(&ctx.trace_id());
131 let span_id = hex_encode_8(&ctx.span_id());
132 let started = std::time::Instant::now();
133
134 let resp = next.run(ctx).await;
135
136 let status = resp.status().as_u16();
137 let duration_ms = started.elapsed().as_millis() as u64;
138
139 if status >= 500 {
140 tracing::error!(
141 trace_id,
142 span_id,
143 method,
144 path,
145 status,
146 duration_ms,
147 "request failed"
148 );
149 } else if status >= 400 {
150 tracing::warn!(
151 trace_id,
152 span_id,
153 method,
154 path,
155 status,
156 duration_ms,
157 "request error"
158 );
159 } else {
160 tracing::info!(
161 trace_id,
162 span_id,
163 method,
164 path,
165 status,
166 duration_ms,
167 "request completed"
168 );
169 }
170
171 resp
172 })
173 }
174}
175
176pub struct TraceInterceptor;
188
189struct SpanGuard<S: opentelemetry::trace::Span>(S);
192
193impl<S: opentelemetry::trace::Span> Drop for SpanGuard<S> {
194 fn drop(&mut self) {
195 self.0.end();
196 }
197}
198
199impl Interceptor for TraceInterceptor {
200 fn around(
201 &'static self,
202 ctx: RequestContext,
203 next: NextHandler,
204 ) -> BoxFuture<'static, Response> {
205 Box::pin(async move {
206 use crate::observability::metrics::record_request;
207 use crate::observability::otel::parent_context_from;
208 use crate::web::context::{hex_encode_16, hex_encode_8};
209 use opentelemetry::trace::{Span, SpanKind, Status, Tracer};
210 use opentelemetry::KeyValue;
211
212 let method = ctx.method().to_string();
213 let route = ctx.route().to_owned();
214 let trace_id = hex_encode_16(&ctx.trace_id());
215 let span_id = hex_encode_8(&ctx.span_id());
216 let started = std::time::Instant::now();
217
218 let parent_cx = parent_context_from(&ctx);
220 let tracer = opentelemetry::global::tracer("arcly-http");
221 let span_name = format!("{method} {route}");
222 let mut span = SpanGuard(
223 tracer
224 .span_builder(span_name)
225 .with_kind(SpanKind::Server)
226 .start_with_context(&tracer, &parent_cx),
227 );
228 span.0
229 .set_attribute(KeyValue::new("http.request.method", method.clone()));
230 span.0
231 .set_attribute(KeyValue::new("http.route", route.clone()));
232
233 let resp = next.run(ctx).await;
235
236 let status = resp.status().as_u16();
238 let elapsed = started.elapsed();
239 let duration_ms = elapsed.as_millis() as u64;
240
241 span.0
242 .set_attribute(KeyValue::new("http.response.status_code", status as i64));
243 if status >= 500 {
244 span.0.set_status(Status::error("server error"));
245 }
246
247 record_request(&route, &method, status, elapsed.as_secs_f64());
249
250 if status >= 500 {
252 tracing::error!(
253 trace_id,
254 span_id,
255 method,
256 route,
257 status,
258 duration_ms,
259 "request failed"
260 );
261 } else if status >= 400 {
262 tracing::warn!(
263 trace_id,
264 span_id,
265 method,
266 route,
267 status,
268 duration_ms,
269 "request error"
270 );
271 } else {
272 tracing::info!(
273 trace_id,
274 span_id,
275 method,
276 route,
277 status,
278 duration_ms,
279 "request completed"
280 );
281 }
282
283 resp
284 })
285 }
286}
287
288pub struct EnvelopeResponse;
291impl Interceptor for EnvelopeResponse {
292 fn around(
293 &'static self,
294 ctx: RequestContext,
295 next: NextHandler,
296 ) -> BoxFuture<'static, Response> {
297 Box::pin(async move {
298 use axum::body::to_bytes;
299 let resp = next.run(ctx).await;
300 if !resp.status().is_success() {
301 return resp;
302 }
303 let (parts, body) = resp.into_parts();
304 let bytes = to_bytes(body, usize::MAX).await.unwrap_or_default();
305 let inner: serde_json::Value =
306 serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null);
307 let wrapped = serde_json::json!({ "data": inner });
308 let body = axum::body::Body::from(serde_json::to_vec(&wrapped).unwrap());
309 Response::from_parts(parts, body)
310 })
311 }
312}