1use std::borrow::Cow;
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_str, parse_traceparent};
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: Vec<String>,
58}
59
60impl LoggingConfig {
61 pub fn production(service_name: impl Into<String>) -> Self {
65 Self {
66 service_name: service_name.into(),
67 version: None,
68 environment: None,
69 format: LoggingFormat::Json,
70 level_filter: "info".to_owned(),
71 redacted_headers: Vec::new(),
72 }
73 }
74
75 pub fn development(service_name: impl Into<String>) -> Self {
80 Self::production(service_name).with_format(LoggingFormat::Pretty)
81 }
82
83 pub fn service_name(&self) -> &str {
85 &self.service_name
86 }
87
88 pub fn version(mut self, version: impl Into<String>) -> Self {
93 self.version = Some(version.into());
94 self
95 }
96
97 pub fn environment(mut self, environment: impl Into<String>) -> Self {
102 self.environment = Some(environment.into());
103 self
104 }
105
106 pub fn with_format(mut self, format: LoggingFormat) -> Self {
108 self.format = format;
109 self
110 }
111
112 pub fn level_filter(mut self, level_filter: impl Into<String>) -> Self {
114 self.level_filter = level_filter.into();
115 self
116 }
117
118 pub fn redact_header(mut self, header: impl AsRef<str>) -> Self {
124 let header = header.as_ref().to_ascii_lowercase();
125 if let Err(index) = self.redacted_headers.binary_search(&header) {
126 self.redacted_headers.insert(index, header);
127 }
128 self
129 }
130
131 pub fn redacts_header(&self, header: impl AsRef<str>) -> bool {
133 let header = header.as_ref();
134 self.redacted_headers
135 .binary_search_by(|configured| {
136 configured
137 .bytes()
138 .cmp(header.bytes().map(|byte| byte.to_ascii_lowercase()))
139 })
140 .is_ok()
141 }
142
143 pub const fn output_format(&self) -> LoggingFormat {
145 self.format
146 }
147
148 pub const fn format(&self) -> LoggingFormat {
150 self.format
151 }
152
153 pub fn service_span(&self) -> Span {
155 tracing::info_span!(
156 "service",
157 service.name = %self.service_name,
158 service.version = %self.version.as_deref().unwrap_or(""),
159 deployment.environment = %self.environment.as_deref().unwrap_or("")
160 )
161 }
162
163 pub fn init(&self) -> Result<(), tracing_subscriber::util::TryInitError> {
169 match self.format {
170 LoggingFormat::Json => self.subscriber_with_writer(std::io::stderr).try_init(),
171 LoggingFormat::Pretty => self
172 .pretty_subscriber_with_writer(std::io::stderr)
173 .try_init(),
174 }
175 }
176
177 pub fn subscriber_with_writer<W>(
179 &self,
180 writer: W,
181 ) -> impl tracing::Subscriber + Send + Sync + 'static
182 where
183 W: for<'writer> MakeWriter<'writer> + Clone + Send + Sync + 'static,
184 {
185 let filter =
186 EnvFilter::try_new(&self.level_filter).unwrap_or_else(|_| EnvFilter::new("info"));
187 let layer = fmt::layer()
188 .json()
189 .flatten_event(true)
190 .with_current_span(true)
191 .with_span_list(false)
192 .with_ansi(false)
193 .with_target(false)
194 .with_writer(writer)
195 .with_filter(filter);
196 Registry::default().with(layer)
197 }
198
199 pub fn pretty_subscriber_with_writer<W>(
201 &self,
202 writer: W,
203 ) -> impl tracing::Subscriber + Send + Sync + 'static
204 where
205 W: for<'writer> MakeWriter<'writer> + Clone + Send + Sync + 'static,
206 {
207 let filter =
208 EnvFilter::try_new(&self.level_filter).unwrap_or_else(|_| EnvFilter::new("info"));
209 let layer = fmt::layer()
210 .pretty()
211 .with_ansi(false)
212 .with_target(false)
213 .with_writer(writer)
214 .with_filter(filter);
215 Registry::default().with(layer)
216 }
217}
218
219#[derive(Clone, Debug)]
227pub struct StructuredMakeSpan {
228 config: LoggingConfig,
229 route: Option<Cow<'static, str>>,
230}
231
232impl StructuredMakeSpan {
233 pub fn new(config: LoggingConfig) -> Self {
235 Self {
236 config,
237 route: None,
238 }
239 }
240
241 pub fn route(mut self, route: impl Into<Cow<'static, str>>) -> Self {
246 self.route = Some(route.into());
247 self
248 }
249}
250
251impl<B> MakeSpan<B> for StructuredMakeSpan {
252 fn make_span(&mut self, request: &Request<B>) -> Span {
253 let request_id = header_to_str(request.headers(), "x-request-id").unwrap_or_default();
254 let trace_id = header_to_str(request.headers(), "traceparent")
255 .and_then(parse_traceparent)
256 .map(|context| context.trace_id)
257 .unwrap_or_default();
258 let route = self
259 .route
260 .as_deref()
261 .or_else(|| {
262 request
263 .extensions()
264 .get::<axum::extract::MatchedPath>()
265 .map(axum::extract::MatchedPath::as_str)
266 })
267 .unwrap_or("<unknown>");
268
269 tracing::span!(
270 Level::INFO,
271 "http.request",
272 service.name = %self.config.service_name,
273 service.version = %self.config.version.as_deref().unwrap_or(""),
274 deployment.environment = %self.config.environment.as_deref().unwrap_or(""),
275 request.id = %request_id,
276 trace.id = %trace_id,
277 http.method = %request.method(),
278 http.route = %route,
279 http.target = %request.uri(),
280 )
281 }
282}