1use std::{borrow::Cow, collections::BTreeSet};
4
5use http::Request;
6use tower_http::trace::MakeSpan;
7use tracing::{Level, Span};
8use tracing_subscriber::{
9 EnvFilter, Layer, Registry,
10 fmt::{self, MakeWriter},
11 layer::SubscriberExt,
12 util::SubscriberInitExt,
13};
14
15use crate::context::header_to_string;
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum LoggingFormat {
20 Json,
22 Pretty,
24}
25
26#[derive(Clone, Debug)]
49pub struct LoggingConfig {
50 service_name: String,
51 version: Option<String>,
52 environment: Option<String>,
53 format: LoggingFormat,
54 level_filter: String,
55 redacted_headers: BTreeSet<String>,
56}
57
58impl LoggingConfig {
59 pub fn production(service_name: impl Into<String>) -> Self {
63 Self {
64 service_name: service_name.into(),
65 version: None,
66 environment: None,
67 format: LoggingFormat::Json,
68 level_filter: "info".to_owned(),
69 redacted_headers: BTreeSet::new(),
70 }
71 }
72
73 pub fn development(service_name: impl Into<String>) -> Self {
78 Self::production(service_name).with_format(LoggingFormat::Pretty)
79 }
80
81 pub fn service_name(&self) -> &str {
83 &self.service_name
84 }
85
86 pub fn version(mut self, version: impl Into<String>) -> Self {
91 self.version = Some(version.into());
92 self
93 }
94
95 pub fn environment(mut self, environment: impl Into<String>) -> Self {
100 self.environment = Some(environment.into());
101 self
102 }
103
104 pub fn with_format(mut self, format: LoggingFormat) -> Self {
106 self.format = format;
107 self
108 }
109
110 pub fn level_filter(mut self, level_filter: impl Into<String>) -> Self {
112 self.level_filter = level_filter.into();
113 self
114 }
115
116 pub fn redact_header(mut self, header: impl AsRef<str>) -> Self {
122 self.redacted_headers
123 .insert(header.as_ref().to_ascii_lowercase());
124 self
125 }
126
127 pub fn redacts_header(&self, header: impl AsRef<str>) -> bool {
129 self.redacted_headers
130 .contains(&header.as_ref().to_ascii_lowercase())
131 }
132
133 pub const fn output_format(&self) -> LoggingFormat {
135 self.format
136 }
137
138 pub const fn format(&self) -> LoggingFormat {
140 self.format
141 }
142
143 pub fn service_span(&self) -> Span {
145 tracing::info_span!(
146 "service",
147 service.name = %self.service_name,
148 service.version = %self.version.as_deref().unwrap_or(""),
149 deployment.environment = %self.environment.as_deref().unwrap_or("")
150 )
151 }
152
153 pub fn init(&self) -> Result<(), tracing_subscriber::util::TryInitError> {
159 match self.format {
160 LoggingFormat::Json => self.subscriber_with_writer(std::io::stderr).try_init(),
161 LoggingFormat::Pretty => self
162 .pretty_subscriber_with_writer(std::io::stderr)
163 .try_init(),
164 }
165 }
166
167 pub fn subscriber_with_writer<W>(
169 &self,
170 writer: W,
171 ) -> impl tracing::Subscriber + Send + Sync + 'static
172 where
173 W: for<'writer> MakeWriter<'writer> + Clone + Send + Sync + 'static,
174 {
175 let filter =
176 EnvFilter::try_new(&self.level_filter).unwrap_or_else(|_| EnvFilter::new("info"));
177 let layer = fmt::layer()
178 .json()
179 .flatten_event(true)
180 .with_current_span(true)
181 .with_span_list(false)
182 .with_ansi(false)
183 .with_target(false)
184 .with_writer(writer)
185 .with_filter(filter);
186 Registry::default().with(layer)
187 }
188
189 pub fn pretty_subscriber_with_writer<W>(
191 &self,
192 writer: W,
193 ) -> impl tracing::Subscriber + Send + Sync + 'static
194 where
195 W: for<'writer> MakeWriter<'writer> + Clone + Send + Sync + 'static,
196 {
197 let filter =
198 EnvFilter::try_new(&self.level_filter).unwrap_or_else(|_| EnvFilter::new("info"));
199 let layer = fmt::layer()
200 .pretty()
201 .with_ansi(false)
202 .with_target(false)
203 .with_writer(writer)
204 .with_filter(filter);
205 Registry::default().with(layer)
206 }
207}
208
209#[derive(Clone, Debug)]
217pub struct StructuredMakeSpan {
218 config: LoggingConfig,
219 route: Option<Cow<'static, str>>,
220}
221
222impl StructuredMakeSpan {
223 pub fn new(config: LoggingConfig) -> Self {
225 Self {
226 config,
227 route: None,
228 }
229 }
230
231 pub fn route(mut self, route: impl Into<Cow<'static, str>>) -> Self {
236 self.route = Some(route.into());
237 self
238 }
239}
240
241impl<B> MakeSpan<B> for StructuredMakeSpan {
242 fn make_span(&mut self, request: &Request<B>) -> Span {
243 let request_id = header_to_string(request.headers(), "x-request-id").unwrap_or_default();
244 let trace_id = header_to_string(request.headers(), "traceparent")
245 .and_then(|value| value.split('-').nth(1).map(str::to_owned))
246 .unwrap_or_default();
247 let route = self
248 .route
249 .as_deref()
250 .map(str::to_owned)
251 .or_else(|| {
252 request
253 .extensions()
254 .get::<axum::extract::MatchedPath>()
255 .map(|path| path.as_str().to_owned())
256 })
257 .unwrap_or_else(|| "<unknown>".to_owned());
258
259 tracing::span!(
260 Level::INFO,
261 "http.request",
262 service.name = %self.config.service_name,
263 service.version = %self.config.version.as_deref().unwrap_or(""),
264 deployment.environment = %self.config.environment.as_deref().unwrap_or(""),
265 request.id = %request_id,
266 trace.id = %trace_id,
267 http.method = %request.method(),
268 http.route = %route,
269 http.target = %request.uri(),
270 )
271 }
272}