lexe_api/trace.rs
1//! This module provides API tracing utilities for both client and server,
2//! including constants and fns which help keep client and server consistent.
3
4use std::{
5 fmt::{self, Display},
6 sync::OnceLock,
7 time::Duration,
8};
9
10use anyhow::{Context, bail, ensure};
11use http::{HeaderName, HeaderValue};
12use lexe_common::time::DisplayMs;
13use lexe_crypto::rng::{RngCore, ThreadFastRng};
14use tracing::{Dispatch, span, warn};
15
16#[cfg(doc)]
17use crate::rest::RestClient;
18
19/// The `target` that should be used for request spans and events.
20// Short, greppable, low chance of collision in logs
21pub(crate) const TARGET: &str = "lxapi";
22
23/// The [`HeaderName`] used to read/write [`TraceId`]s.
24pub(crate) static TRACE_ID_HEADER_NAME: HeaderName =
25 HeaderName::from_static("lexe-trace-id");
26
27/// A [`TraceId`] identifies a tree of requests sharing a single causal source
28/// as it travels between different Lexe services.
29/// - It is generated by the originating client and propagated via HTTP headers
30/// between services, and via tracing span `Extensions` within services.
31/// - Each [`TraceId`] consists of 16 alphanumeric bytes (a-z,A-Z,0-9); all
32/// [`TraceId`] constructors must enforce this invariant.
33#[derive(Clone, PartialEq)]
34pub struct TraceId(HeaderValue);
35
36/// A function pointer to a fn which attempts to extract a [`TraceId`] from the
37/// `Extensions` of the given span or any of its parents.
38///
39/// This static is required because we are required to downcast the [`Dispatch`]
40/// to a concrete subscriber type (which implements the `LookupSpan` trait),
41/// which we do not know since we are agnostic over [`tracing::Subscriber`]s.
42/// An alternative was to make the [`RestClient`] generic over a logger `L` so
43/// that the subscriber type was known when calling `client::request_span`.
44/// However, this results in highly undesirable [`RestClient`] ergonomics.
45/// Instead, we provide impls for these fns via the `define_trace_id_fns` macro,
46/// and initialize these statics in the `try_init()` of our loggers.
47pub static GET_TRACE_ID_FN: OnceLock<
48 fn(&span::Id, &Dispatch) -> anyhow::Result<Option<TraceId>>,
49> = OnceLock::new();
50/// Like [`GET_TRACE_ID_FN`], but inserts a [`TraceId`] into the `Extensions`
51/// of a given span, returning the replaced [`TraceId`] if it existed.
52pub static INSERT_TRACE_ID_FN: OnceLock<
53 fn(&span::Id, &Dispatch, TraceId) -> anyhow::Result<Option<TraceId>>,
54> = OnceLock::new();
55
56impl TraceId {
57 /// Byte length of a [`TraceId`].
58 const LENGTH: usize = 16;
59
60 /// Convenience to generate a [`TraceId`] using the thread-local
61 /// [`ThreadFastRng`].
62 pub fn generate() -> Self {
63 Self::from_rng(&mut ThreadFastRng::new())
64 }
65
66 /// Generate a [`TraceId`] from an existing rng.
67 pub fn from_rng(rng: &mut impl RngCore) -> Self {
68 use lexe_crypto::rng::RngExt;
69
70 // Generate a 16 byte array with alphanumeric characters
71 let buf: [u8; Self::LENGTH] = rng.gen_alphanum_bytes();
72
73 let header_value = HeaderValue::from_bytes(&buf).expect(
74 "All alphanumeric bytes are in range (32..=255), \
75 and none are byte 127 (DEL). This is also checked in tests.",
76 );
77
78 Self(header_value)
79 }
80
81 /// Get this [`TraceId`] as a [`&str`].
82 pub fn as_str(&self) -> &str {
83 debug_assert!(std::str::from_utf8(self.0.as_bytes()).is_ok());
84 // SAFETY: All constructors ensure that all bytes are alphanumeric
85 unsafe { std::str::from_utf8_unchecked(self.0.as_bytes()) }
86 }
87
88 /// Get a corresponding [`HeaderValue`].
89 pub fn to_header_value(&self) -> HeaderValue {
90 self.0.clone()
91 }
92
93 /// Get the [`TraceId`] from the `Extensions` of the given span or any of
94 /// its parents, logging any errors that occur as warnings.
95 fn get_from_span(span: &tracing::Span) -> Option<Self> {
96 // Tests are usually not instrumented with tracing spans. To prevent
97 // tests from spamming "WARN: Span is not enabled", we return early if
98 // the given span was disabled and we are in test. In prod, however,
99 // ~everything should be instrumented, so we do want to log the `WARN`s.
100 #[cfg(any(test, feature = "test-utils"))]
101 if span.is_disabled() {
102 return None;
103 }
104
105 let try_get_trace_id = || {
106 // Fetch the `get_trace_id_from_span` fn pointer from the static.
107 let get_trace_id_fn = GET_TRACE_ID_FN.get().context(
108 "GET_TRACE_ID_FN not set. Did lexe_logger::try_init() \
109 initialize the TraceId statics?",
110 )?;
111 let maybe_trace_id = span
112 // Here, we actually call the fn to try to get the trace id.
113 .with_subscriber(|(id, dispatch)| get_trace_id_fn(id, dispatch))
114 .context("Span is not enabled")?
115 .context("get_trace_id_fn (get_trace_id_from_span) failed")?;
116
117 Ok::<_, anyhow::Error>(maybe_trace_id)
118 };
119
120 try_get_trace_id()
121 .inspect_err(|e| warn!("Failed to check for trace id: {e:#}"))
122 .unwrap_or_default()
123 }
124
125 /// Insert this [`TraceId`] into the `Extensions` of the given span,
126 /// logging any errors that occur as warnings. Also logs a warning if a
127 /// [`TraceId`] already existed in the span and was replaced by this insert.
128 fn insert_into_span(self, span: &tracing::Span) {
129 let try_insert_trace_id = || {
130 // Fetch the `insert_trace_id_into_span` fn pointer from the static.
131 let insert_trace_id_fn = INSERT_TRACE_ID_FN.get().context(
132 "INSERT_TRACE_ID_FN not set. Did lexe_logger::try_init() \
133 initialize the TraceId statics?",
134 )?;
135
136 let maybe_replaced = span
137 // Here, we actually call the fn to try to insert the trace id.
138 .with_subscriber(|(id, dispatch)| {
139 insert_trace_id_fn(id, dispatch, self)
140 })
141 .context("Span is not enabled")?
142 .context("insert_trace_id_into_span failed")?;
143
144 Ok::<_, anyhow::Error>(maybe_replaced)
145 };
146
147 try_insert_trace_id()
148 // TODO(phlip9): fix `TraceId` when RUST_LOG is `warn` or `error`
149 // .inspect_err(|e| warn!("Failed to insert trace id: {e:#}"))
150 .unwrap_or_default()
151 .inspect(|replaced| warn!("Replaced existing TraceId: {replaced}"));
152 }
153
154 /// A test implementation which can be used to test that getting and setting
155 /// [`TraceId`]s from and into span `Extensions` works with a specific
156 /// [`Subscriber`]. Call it after logger init like so:
157 ///
158 /// ```ignore
159 /// #[test]
160 /// fn get_and_insert_trace_ids() {
161 /// let _ = try_init();
162 /// TraceId::get_and_insert_test_impl();
163 /// }
164 /// ```
165 ///
166 /// NOTE: This impl downcasts [`tracing::Dispatch`] to the subscriber type
167 /// specified during logger init, which fails if the global logger has been
168 /// set to a different [`Subscriber`] type than the one expected in this
169 /// test, which may happen if multiple tests are run in parallel in the same
170 /// process with different [`Subscriber`]s. Thankfully, cargo test builds
171 /// separate test binaries for each crate and then runs each (serially) as
172 /// its own process. This should usually prevent conflicts since each crate
173 /// usually only uses one [`Subscriber`] type, but if a test starts flaking
174 /// because of this, feel free to just `#[ignore]` the test, and manually
175 /// run the test only when making changes to the logging / tracing setup.
176 ///
177 /// [`Subscriber`]: tracing::Subscriber
178 #[cfg(any(test, feature = "test-utils"))]
179 pub fn get_and_insert_test_impl() {
180 use tracing::{error_span, info};
181
182 GET_TRACE_ID_FN.get().expect("GET_TRACE_ID_FN not set");
183 INSERT_TRACE_ID_FN
184 .get()
185 .expect("INSERT_TRACE_ID_FN not set");
186
187 let trace_id1 = TraceId::generate();
188
189 // Use error spans so that the test still passes with `RUST_LOG=error`.
190 let outer_span = error_span!("(outer)", trace_id=%trace_id1);
191
192 // Sanity check: get_from_span should return no TraceId.
193 assert!(TraceId::get_from_span(&outer_span).is_none());
194
195 // Insert the TraceId into the outer span's extensions.
196 trace_id1.clone().insert_into_span(&outer_span);
197
198 // Enter the outer span.
199 outer_span.in_scope(|| {
200 info!("This msg should contain (outer) and `trace_id`");
201
202 // We should be able to recover the trace id within the outer span.
203 let current_span = tracing::Span::current();
204 let trace_id2 = TraceId::get_from_span(¤t_span)
205 .expect("No trace id returned");
206 assert_eq!(trace_id1, trace_id2);
207
208 // Create an inner span. trace_id should've been set by the parent.
209 let inner_span =
210 error_span!("(inner)", trace_id = tracing::field::Empty);
211
212 // Enter the inner span.
213 inner_span.in_scope(|| {
214 info!("This msg should have (outer):(inner) and `trace_id`");
215
216 // Should be able to recover the trace id within the inner span.
217 let current_span = tracing::Span::current();
218 let trace_id3 = TraceId::get_from_span(¤t_span)
219 .expect("No trace id returned");
220 assert_eq!(trace_id2, trace_id3);
221 });
222 });
223
224 info!("Test complete");
225 }
226}
227
228impl TryFrom<HeaderValue> for TraceId {
229 type Error = anyhow::Error;
230
231 fn try_from(src: HeaderValue) -> Result<Self, Self::Error> {
232 let src_bytes = src.as_bytes();
233 if src_bytes.len() != Self::LENGTH {
234 bail!("Source header value had wrong length");
235 }
236
237 let all_alphanumeric = src_bytes
238 .iter()
239 .all(|byte| char::is_alphanumeric(*byte as char));
240 ensure!(
241 all_alphanumeric,
242 "Source header value contained non-alphanumeric bytes"
243 );
244
245 Ok(Self(src))
246 }
247}
248
249impl Display for TraceId {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 write!(f, "{}", self.as_str())
252 }
253}
254
255impl fmt::Debug for TraceId {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 write!(f, "{self}")
258 }
259}
260
261#[cfg(any(test, feature = "test-utils"))]
262mod arbitrary_impl {
263 use lexe_crypto::rng::FastRng;
264 use proptest::{
265 arbitrary::{Arbitrary, any},
266 strategy::{BoxedStrategy, Strategy},
267 };
268
269 use super::*;
270
271 impl Arbitrary for TraceId {
272 type Parameters = ();
273 type Strategy = BoxedStrategy<Self>;
274 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
275 any::<FastRng>()
276 .prop_map(|mut rng| Self::from_rng(&mut rng))
277 .boxed()
278 }
279 }
280}
281
282/// Generates implementations of the functions pointed to by [`GET_TRACE_ID_FN`]
283/// and [`INSERT_TRACE_ID_FN`] given the type of the [`tracing::Subscriber`].
284/// The caller (typically `lexe_logger::try_init()`) is responsible for
285/// initializing these statics using the generated implementations.
286///
287/// ```ignore
288/// # use anyhow::{anyhow, Context};
289/// # use tracing_subscriber::{util::SubscriberInitExt, FmtSubscriber};
290/// #
291/// pub fn try_init() -> anyhow::Result<()> {
292/// FmtSubscriber::new().try_init().context("Logger already set")?;
293///
294/// // Notice how `FmtSubscriber` here exactly matches our subscriber type.
295/// // If using a more complex subscriber, you will have to name the type,
296/// // e.g. `Layered<Filtered<FmtLayer<Registry, ...>, ..., ...>, ...>`.
297/// // See public/logger/src/lib.rs for an example of this.
298/// lexe_common::define_trace_id_fns!(FmtSubscriber);
299/// lexe_api::trace::GET_TRACE_ID_FN
300/// .set(get_trace_id_from_span)
301/// .map_err(|_| anyhow!("GET_TRACE_ID_FN already set"))?;
302/// lexe_api::trace::INSERT_TRACE_ID_FN
303/// .set(insert_trace_id_into_span)
304/// .map_err(|_| anyhow!("INSERT_TRACE_ID_FN already set"))?;
305///
306/// Ok(())
307/// }
308/// ```
309#[macro_export]
310macro_rules! define_trace_id_fns {
311 ($subscriber:ty) => {
312 use anyhow::Context;
313 use lexe_api::trace::TraceId;
314 use tracing_subscriber::registry::LookupSpan;
315
316 /// Get the [`TraceId`] from the `Extensions` of this span or any of its
317 /// parents. Errors if downcasting to the subscriber fails, or if the
318 /// subscriber doesn't return a `SpanRef` for the given span id.
319 fn get_trace_id_from_span(
320 id: &tracing::span::Id,
321 dispatch: &tracing::Dispatch,
322 ) -> anyhow::Result<Option<TraceId>> {
323 let subscriber = dispatch.downcast_ref::<$subscriber>().context(
324 "Downcast failed. Did lexe_logger::try_init() define the trace_id \
325 fns with the correct subscriber type?",
326 )?;
327 let span_ref = subscriber
328 .span(id)
329 .context("Failed to get SpanRef from id")?;
330 let maybe_trace_id = span_ref
331 .scope()
332 .find_map(|span| span.extensions().get::<TraceId>().cloned());
333 Ok(maybe_trace_id)
334 }
335
336 /// Insert the [`TraceId`] into the `Extensions` of the given span.
337 /// Errors if downcasting to the subscriber fails, or if the
338 /// subscriber doesn't return a `SpanRef` for the given span id.
339 /// Returns the replaced [`TraceId`] if one already existed in the span.
340 fn insert_trace_id_into_span(
341 id: &tracing::span::Id,
342 dispatch: &tracing::Dispatch,
343 trace_id: TraceId,
344 ) -> anyhow::Result<Option<TraceId>> {
345 let subscriber = dispatch.downcast_ref::<$subscriber>().context(
346 "Downcast failed. Did lexe_logger::try_init() define the trace_id \
347 fns with the correct subscriber type?",
348 )?;
349 let span_ref = subscriber.span(id).context("No span ref for id")?;
350 let maybe_replaced = span_ref.extensions_mut().replace(trace_id);
351 Ok(maybe_replaced)
352 }
353 };
354}
355
356/// Client tracing utilities.
357pub(crate) mod client {
358 use tracing::info_span;
359
360 use super::*;
361
362 /// Get a [`tracing::Span`] and [`TraceId`] for a client request.
363 pub(crate) fn request_span(
364 req: &reqwest::Request,
365 from: &str,
366 to: &'static str,
367 ) -> (tracing::Span, TraceId) {
368 // Our client request span.
369 let request_span = info_span!(
370 target: TARGET,
371 "(req)(cli)",
372 // This does not overwrite existing values if the field was already
373 // set by a parent span. If it was not set, we will record it below.
374 trace_id = tracing::field::Empty,
375 %from,
376 %to,
377 method = %req.method(),
378 url = %req.url(),
379 // This is set later in the request
380 attempts_left = tracing::field::Empty,
381 );
382
383 // Check if a parent span already has a trace_id set in its Extensions,
384 // and extract it if so. This happens when an outgoing client request is
385 // made in the API handler of a server which has created a (server-side)
386 // request span for a (client) request which included a trace id header.
387 let existing_trace_id =
388 TraceId::get_from_span(&tracing::Span::current());
389
390 // If we found a TraceId from a parent span, the parent will already
391 // have set the trace_id field and included it in its Extensions, so
392 // there is nothing to do. Otherwise, we need to generate a new TraceId,
393 // record it in the trace_id field, and add it our span's Extensions.
394 // NOTE: Setting the trace_id field here after it has already been set
395 // by a parent will result in duplicate trace_id keys in logs.
396 let trace_id = match existing_trace_id {
397 Some(tid) => tid,
398 None => {
399 let trace_id = TraceId::generate();
400 request_span.record("trace_id", trace_id.as_str());
401
402 trace_id.clone().insert_into_span(&request_span);
403
404 trace_id
405 }
406 };
407
408 (request_span, trace_id)
409 }
410}
411
412/// Server tracing utilities.
413pub(crate) mod server {
414 use anyhow::anyhow;
415 use http::header::USER_AGENT;
416 use tower_http::{
417 classify::{
418 ClassifiedResponse, ClassifyResponse, NeverClassifyEos,
419 SharedClassifier,
420 },
421 trace::{
422 MakeSpan, OnEos, OnFailure, OnRequest, OnResponse, TraceLayer,
423 },
424 };
425 use tracing::{debug, error, info_span, warn};
426
427 use super::*;
428
429 /// Builds a [`TraceLayer`] which:
430 ///
431 /// - Instruments each incoming request with its own request span, reusing
432 /// the [`TraceId`] from the `lexe-trace-id` header if available
433 /// - Logs "New server request" at the start of each received request
434 /// - Logs "Done (result) after the completion of each response"
435 /// - Logs "Stream ended" when streaming bodies (post-response) complete
436 /// - Logs "Other failure" whenever anything else goes wrong
437 ///
438 /// It can be passed to e.g. [`Router::layer`] or [`ServiceBuilder::layer`].
439 ///
440 /// [`Router::layer`]: axum::Router::layer
441 /// [`ServiceBuilder::layer`]: tower::ServiceBuilder::layer
442 pub(crate) fn trace_layer(
443 api_span: tracing::Span,
444 ) -> TraceLayer<
445 SharedClassifier<LxClassifyResponse>,
446 LxMakeSpan,
447 LxOnRequest,
448 LxOnResponse,
449 (),
450 LxOnEos,
451 LxOnFailure,
452 > {
453 // `tower_http::trace` documents when each of these callbacks is called.
454 TraceLayer::new(SharedClassifier::new(LxClassifyResponse))
455 .make_span_with(LxMakeSpan { api_span })
456 .on_request(LxOnRequest)
457 .on_response(LxOnResponse)
458 // Do nothing on body chunk
459 .on_body_chunk(())
460 .on_eos(LxOnEos)
461 .on_failure(LxOnFailure)
462 }
463
464 /// A [`ClassifyResponse`] which classifies all responses as OK.
465 ///
466 /// We do this because all responses (including error responses) are already
467 /// logged by [`LxOnResponse`], so triggering [`OnFailure`] on error status
468 /// codes (the default impl for HTTP) would result in redundant logs.
469 #[derive(Clone)]
470 pub(crate) struct LxClassifyResponse;
471
472 impl ClassifyResponse for LxClassifyResponse {
473 type FailureClass = anyhow::Error;
474 type ClassifyEos = NeverClassifyEos<Self::FailureClass>;
475
476 fn classify_response<B>(
477 self,
478 _response: &http::Response<B>,
479 ) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos> {
480 ClassifiedResponse::Ready(Ok(()))
481 }
482
483 fn classify_error<E: Display + 'static>(
484 self,
485 error: &E,
486 ) -> Self::FailureClass {
487 anyhow!("{error:#}")
488 }
489 }
490
491 /// A [`MakeSpan`] impl which mirrors [`client::request_span`].
492 #[derive(Clone)]
493 pub(crate) struct LxMakeSpan {
494 /// The server API span, used as each request span's parent.
495 api_span: tracing::Span,
496 }
497
498 impl<B> MakeSpan<B> for LxMakeSpan {
499 fn make_span(&mut self, request: &http::Request<B>) -> tracing::Span {
500 // Get the full url, including query params
501 let url = request
502 .uri()
503 .path_and_query()
504 .map(|url| url.as_str())
505 .unwrap_or("/");
506
507 // Parse the client-provided trace id from the trace id header.
508 // Generate a new trace id if none existed or if the header value
509 // was invalid. There should be no need to try to get a TraceId from
510 // a parent span's Extensions.
511 let trace_id = request
512 .headers()
513 .get(&TRACE_ID_HEADER_NAME)
514 .and_then(|value| TraceId::try_from(value.clone()).ok())
515 .unwrap_or_else(TraceId::generate);
516
517 // Log the user agent header as `from` if it exists, or a default.
518 // This is the same `from` as in the RestClient and client span.
519 // The `to` field does not need to be logged since it is already
520 // included as part of the server's span name.
521 let from = request
522 .headers()
523 .get(USER_AGENT)
524 .map(|value| value.to_str().unwrap_or("(non-ascii)"))
525 .unwrap_or("(none)");
526
527 let request_span = info_span!(
528 target: TARGET,
529 parent: self.api_span.clone(),
530 "(req)(srv)",
531 %trace_id,
532 %from,
533 method = %request.method().as_str(),
534 url = %url,
535 version = ?request.version(),
536 );
537
538 // Insert the trace id into the server request span's `Extensions`,
539 // so that any client requests made in our handler can pick it up.
540 trace_id.insert_into_span(&request_span);
541
542 request_span
543 }
544 }
545
546 /// `OnRequest` impl mirroring `RestClient::send_inner`.
547 #[derive(Clone)]
548 pub(crate) struct LxOnRequest;
549
550 impl<B> OnRequest<B> for LxOnRequest {
551 fn on_request(
552 &mut self,
553 request: &http::Request<B>,
554 _request_span: &tracing::Span,
555 ) {
556 let headers = request.headers();
557 debug!(target: TARGET, "New server request");
558 debug!(target: TARGET, ?headers, "Server request (headers)");
559 }
560 }
561
562 /// [`OnResponse`] impl which logs the completion of requests by the server.
563 /// `RestClient` logs `req_time`; analogously here we log `resp_time`.
564 #[derive(Clone)]
565 pub(crate) struct LxOnResponse;
566
567 impl<B> OnResponse<B> for LxOnResponse {
568 fn on_response(
569 self,
570 response: &http::Response<B>,
571 // Client logs "req_time", server logs "resp_time"
572 resp_time: Duration,
573 _request_span: &tracing::Span,
574 ) {
575 let status = response.status();
576 let headers = response.headers();
577 let resp_time = DisplayMs(resp_time);
578
579 if status.is_success() {
580 // NOTE: This server request log can be at INFO.
581 // It's cluttering our logs though, so we're suppressing.
582 debug!(target: TARGET, %resp_time, ?status, "Done (success)");
583 } else if status.is_client_error() {
584 warn!(target: TARGET, %resp_time, ?status, "Done (client error)");
585 } else if status.is_server_error() && status.as_u16() == 503 {
586 // Don't spam ERRORs for 503 "Service Unavailable"s which we
587 // return when load-shedding requests. ERRORs should be serious.
588 warn!(target: TARGET, %resp_time, ?status, "Done (load shedded)");
589 } else if status.is_server_error() {
590 error!(target: TARGET, %resp_time, ?status, "Done (server error)");
591 } else {
592 // NOTE: This server request log can be at INFO.
593 // It's cluttering our logs though, so we're suppressing.
594 debug!(target: TARGET, %resp_time, ?status, "Done (other)");
595 }
596
597 // Log the headers too, but only at DEBUG.
598 debug!(
599 target: TARGET, %resp_time, ?status, ?headers,
600 "Done (headers)",
601 );
602 }
603 }
604
605 /// Basic [`OnEos`] impl; we don't stream atm but this will work if we do
606 #[derive(Clone)]
607 pub(crate) struct LxOnEos;
608
609 impl OnEos for LxOnEos {
610 fn on_eos(
611 self,
612 trailers: Option<&http::HeaderMap>,
613 // The duration since the response was sent
614 stream_time: Duration,
615 _request_span: &tracing::Span,
616 ) {
617 let num_trailers = trailers.map(|trailers| trailers.len());
618 let stream_time = DisplayMs(stream_time);
619 debug!(target: TARGET, %stream_time, ?num_trailers, "Stream ended");
620 }
621 }
622
623 /// [`OnFailure`] impl which logs failures. Since [`LxClassifyResponse`]
624 /// does not classify any responses (including error responses) or
625 /// end-of-streams as failures, this will trigger only if the inner
626 /// [`tower::Service`] future resolves to an error, or if `Body::poll_frame`
627 /// returns an error. See [`tower_http::trace`] module docs for more info.
628 #[derive(Clone)]
629 pub(crate) struct LxOnFailure;
630
631 impl<FailureClass: Display> OnFailure<FailureClass> for LxOnFailure {
632 fn on_failure(
633 &mut self,
634 fail_class: FailureClass,
635 // The duration since the request was received
636 fail_time: Duration,
637 _request_span: &tracing::Span,
638 ) {
639 let fail_time = DisplayMs(fail_time);
640 warn!(target: TARGET, %fail_time, %fail_class, "Other failure");
641 }
642 }
643}
644
645#[cfg(test)]
646mod test {
647 use proptest::{prop_assert_eq, proptest};
648
649 use super::*;
650
651 #[test]
652 fn trace_id_proptest() {
653 // TraceId's Arbitrary impl uses TraceId::from_rng
654 proptest!(|(id1: TraceId)| {
655 // Ensure the debug_assert! in TraceId::as_str() doesn't panic
656 id1.as_str();
657 // TraceId -> HeaderValue -> TraceId
658 let id2 = TraceId::try_from(id1.to_header_value()).unwrap();
659 prop_assert_eq!(&id1, &id2);
660 });
661 }
662}