Skip to main content

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(&current_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(&current_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}