1use axum::{http::Request, Router};
4use metrics_exporter_prometheus::PrometheusHandle;
5use tower_http::request_id::{MakeRequestId, RequestId};
6
7use crate::metrics::{metrics_handler, middleware::record_http_metrics};
8
9#[derive(Clone, Copy)]
15struct EchoOnly;
16
17impl MakeRequestId for EchoOnly {
18 #[inline]
19 fn make_request_id<B>(&mut self, _request: &Request<B>) -> Option<RequestId> {
20 None
21 }
22}
23
24#[inline]
26pub fn router() -> Router {
27 use axum::extract::DefaultBodyLimit;
28 use axum::http::header::HeaderName;
29 use axum::middleware;
30 use axum::routing::{get, post};
31 use tower::ServiceBuilder;
32 use tower_http::request_id::{MakeRequestUuid, PropagateRequestIdLayer, SetRequestIdLayer};
33 use tower_http::trace::{DefaultOnFailure, TraceLayer};
34
35 use crate::cache::cache_control_layer;
36 use crate::handlers::{
37 conversion::{options_conversion, post_conversion},
38 fallback::{conversion_method_not_allowed, health_method_not_allowed, not_found},
39 health::{get_health, head_health, options_health},
40 };
41
42 #[cfg(feature = "sentry")]
45 use crate::telemetry;
46 #[cfg(feature = "sentry")]
47 use axum::body::Body;
48 #[cfg(feature = "sentry")]
49 use axum::middleware::Next;
50 #[cfg(feature = "sentry")]
51 use tower::util::option_layer;
52
53 let x_request_id = HeaderName::from_static("x-request-id");
54 let x_trace_id = HeaderName::from_static("x-trace-id");
55
56 let conversion_route = post(post_conversion)
57 .options(options_conversion)
58 .fallback(conversion_method_not_allowed);
59
60 let health_route = get(get_health)
61 .head(head_health)
62 .options(options_health)
63 .fallback(health_method_not_allowed);
64
65 #[cfg(feature = "sentry")]
66 let attach_sentry_tags = |request: Request<Body>, next: Next| async move {
67 if sentry::Hub::current().client().is_some() {
68 let request_id = request
69 .extensions()
70 .get::<RequestId>()
71 .and_then(|req_id| req_id.header_value().to_str().ok())
72 .unwrap_or_default()
73 .to_owned();
74 let trace_id = request
75 .headers()
76 .get("x-trace-id")
77 .and_then(|head| head.to_str().ok())
78 .unwrap_or_default()
79 .to_owned();
80 sentry::configure_scope(|scope| {
81 if !request_id.is_empty() {
82 scope.set_tag("request_id", &request_id);
83 }
84 if !trace_id.is_empty() {
85 scope.set_tag("trace_id", &trace_id);
86 }
87 });
88 }
89 next.run(request).await
90 };
91
92 #[cfg(feature = "sentry")]
98 let middleware_stack = ServiceBuilder::new()
99 .layer(DefaultBodyLimit::disable())
105 .layer(SetRequestIdLayer::new(
106 x_request_id.clone(),
107 MakeRequestUuid,
108 ))
109 .layer(SetRequestIdLayer::new(x_trace_id.clone(), EchoOnly))
110 .layer(option_layer(telemetry::tower_new_layer()))
111 .layer(option_layer(telemetry::tower_http_layer()))
112 .layer(middleware::from_fn(attach_sentry_tags))
113 .layer(
114 TraceLayer::new_for_http()
115 .on_failure(DefaultOnFailure::new().level(tracing::Level::DEBUG)),
116 )
117 .layer(middleware::from_fn(record_http_metrics))
118 .layer(PropagateRequestIdLayer::new(x_request_id))
119 .layer(PropagateRequestIdLayer::new(x_trace_id))
120 .layer(cache_control_layer());
121
122 #[cfg(not(feature = "sentry"))]
123 let middleware_stack = ServiceBuilder::new()
124 .layer(DefaultBodyLimit::disable())
130 .layer(SetRequestIdLayer::new(
131 x_request_id.clone(),
132 MakeRequestUuid,
133 ))
134 .layer(SetRequestIdLayer::new(x_trace_id.clone(), EchoOnly))
135 .layer(
136 TraceLayer::new_for_http()
137 .on_failure(DefaultOnFailure::new().level(tracing::Level::DEBUG)),
138 )
139 .layer(middleware::from_fn(record_http_metrics))
140 .layer(PropagateRequestIdLayer::new(x_request_id))
141 .layer(PropagateRequestIdLayer::new(x_trace_id))
142 .layer(cache_control_layer());
143
144 Router::new()
145 .route("/conversion", conversion_route)
146 .route("/health", health_route)
147 .fallback(not_found)
148 .layer(middleware_stack)
149}
150
151#[inline]
165pub fn router_with_metrics(handle: PrometheusHandle) -> Router {
166 router().route("/metrics", axum::routing::get(metrics_handler(handle)))
167}