lexe-api 0.1.7

Lexe API types, definitions, client/server utils, and TLS config
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
//! This module provides API tracing utilities for both client and server,
//! including constants and fns which help keep client and server consistent.

use std::{
    fmt::{self, Display},
    sync::OnceLock,
    time::Duration,
};

use anyhow::{Context, bail, ensure};
use http::{HeaderName, HeaderValue};
use lexe_common::time::DisplayMs;
use lexe_crypto::rng::{RngCore, ThreadFastRng};
use tracing::{Dispatch, span, warn};

#[cfg(doc)]
use crate::rest::RestClient;

/// The `target` that should be used for request spans and events.
// Short, greppable, low chance of collision in logs
pub(crate) const TARGET: &str = "lxapi";

/// The [`HeaderName`] used to read/write [`TraceId`]s.
pub(crate) static TRACE_ID_HEADER_NAME: HeaderName =
    HeaderName::from_static("lexe-trace-id");

/// A [`TraceId`] identifies a tree of requests sharing a single causal source
/// as it travels between different Lexe services.
/// - It is generated by the originating client and propagated via HTTP headers
///   between services, and via tracing span `Extensions` within services.
/// - Each [`TraceId`] consists of 16 alphanumeric bytes (a-z,A-Z,0-9); all
///   [`TraceId`] constructors must enforce this invariant.
#[derive(Clone, PartialEq)]
pub struct TraceId(HeaderValue);

/// A function pointer to a fn which attempts to extract a [`TraceId`] from the
/// `Extensions` of the given span or any of its parents.
///
/// This static is required because we are required to downcast the [`Dispatch`]
/// to a concrete subscriber type (which implements the `LookupSpan` trait),
/// which we do not know since we are agnostic over [`tracing::Subscriber`]s.
/// An alternative was to make the [`RestClient`] generic over a logger `L` so
/// that the subscriber type was known when calling `client::request_span`.
/// However, this results in highly undesirable [`RestClient`] ergonomics.
/// Instead, we provide impls for these fns via the `define_trace_id_fns` macro,
/// and initialize these statics in the `try_init()` of our loggers.
pub static GET_TRACE_ID_FN: OnceLock<
    fn(&span::Id, &Dispatch) -> anyhow::Result<Option<TraceId>>,
> = OnceLock::new();
/// Like [`GET_TRACE_ID_FN`], but inserts a [`TraceId`] into the `Extensions`
/// of a given span, returning the replaced [`TraceId`] if it existed.
pub static INSERT_TRACE_ID_FN: OnceLock<
    fn(&span::Id, &Dispatch, TraceId) -> anyhow::Result<Option<TraceId>>,
> = OnceLock::new();

impl TraceId {
    /// Byte length of a [`TraceId`].
    const LENGTH: usize = 16;

    /// Convenience to generate a [`TraceId`] using the thread-local
    /// [`ThreadFastRng`].
    pub fn generate() -> Self {
        Self::from_rng(&mut ThreadFastRng::new())
    }

    /// Generate a [`TraceId`] from an existing rng.
    pub fn from_rng(rng: &mut impl RngCore) -> Self {
        use lexe_crypto::rng::RngExt;

        // Generate a 16 byte array with alphanumeric characters
        let buf: [u8; Self::LENGTH] = rng.gen_alphanum_bytes();

        let header_value = HeaderValue::from_bytes(&buf).expect(
            "All alphanumeric bytes are in range (32..=255), \
             and none are byte 127 (DEL). This is also checked in tests.",
        );

        Self(header_value)
    }

    /// Get this [`TraceId`] as a [`&str`].
    pub fn as_str(&self) -> &str {
        debug_assert!(std::str::from_utf8(self.0.as_bytes()).is_ok());
        // SAFETY: All constructors ensure that all bytes are alphanumeric
        unsafe { std::str::from_utf8_unchecked(self.0.as_bytes()) }
    }

    /// Get a corresponding [`HeaderValue`].
    pub fn to_header_value(&self) -> HeaderValue {
        self.0.clone()
    }

    /// Get the [`TraceId`] from the `Extensions` of the given span or any of
    /// its parents, logging any errors that occur as warnings.
    fn get_from_span(span: &tracing::Span) -> Option<Self> {
        // Tests are usually not instrumented with tracing spans. To prevent
        // tests from spamming "WARN: Span is not enabled", we return early if
        // the given span was disabled and we are in test. In prod, however,
        // ~everything should be instrumented, so we do want to log the `WARN`s.
        #[cfg(any(test, feature = "test-utils"))]
        if span.is_disabled() {
            return None;
        }

        let try_get_trace_id = || {
            // Fetch the `get_trace_id_from_span` fn pointer from the static.
            let get_trace_id_fn = GET_TRACE_ID_FN.get().context(
                "GET_TRACE_ID_FN not set. Did lexe_logger::try_init() \
                 initialize the TraceId statics?",
            )?;
            let maybe_trace_id = span
                // Here, we actually call the fn to try to get the trace id.
                .with_subscriber(|(id, dispatch)| get_trace_id_fn(id, dispatch))
                .context("Span is not enabled")?
                .context("get_trace_id_fn (get_trace_id_from_span) failed")?;

            Ok::<_, anyhow::Error>(maybe_trace_id)
        };

        try_get_trace_id()
            .inspect_err(|e| warn!("Failed to check for trace id: {e:#}"))
            .unwrap_or_default()
    }

    /// Insert this [`TraceId`] into the `Extensions` of the given span,
    /// logging any errors that occur as warnings. Also logs a warning if a
    /// [`TraceId`] already existed in the span and was replaced by this insert.
    fn insert_into_span(self, span: &tracing::Span) {
        let try_insert_trace_id = || {
            // Fetch the `insert_trace_id_into_span` fn pointer from the static.
            let insert_trace_id_fn = INSERT_TRACE_ID_FN.get().context(
                "INSERT_TRACE_ID_FN not set. Did lexe_logger::try_init() \
                 initialize the TraceId statics?",
            )?;

            let maybe_replaced = span
                // Here, we actually call the fn to try to insert the trace id.
                .with_subscriber(|(id, dispatch)| {
                    insert_trace_id_fn(id, dispatch, self)
                })
                .context("Span is not enabled")?
                .context("insert_trace_id_into_span failed")?;

            Ok::<_, anyhow::Error>(maybe_replaced)
        };

        try_insert_trace_id()
            // TODO(phlip9): fix `TraceId` when RUST_LOG is `warn` or `error`
            // .inspect_err(|e| warn!("Failed to insert trace id: {e:#}"))
            .unwrap_or_default()
            .inspect(|replaced| warn!("Replaced existing TraceId: {replaced}"));
    }

    /// A test implementation which can be used to test that getting and setting
    /// [`TraceId`]s from and into span `Extensions` works with a specific
    /// [`Subscriber`]. Call it after logger init like so:
    ///
    /// ```ignore
    /// #[test]
    /// fn get_and_insert_trace_ids() {
    ///     let _ = try_init();
    ///     TraceId::get_and_insert_test_impl();
    /// }
    /// ```
    ///
    /// NOTE: This impl downcasts [`tracing::Dispatch`] to the subscriber type
    /// specified during logger init, which fails if the global logger has been
    /// set to a different [`Subscriber`] type than the one expected in this
    /// test, which may happen if multiple tests are run in parallel in the same
    /// process with different [`Subscriber`]s. Thankfully, cargo test builds
    /// separate test binaries for each crate and then runs each (serially) as
    /// its own process. This should usually prevent conflicts since each crate
    /// usually only uses one [`Subscriber`] type, but if a test starts flaking
    /// because of this, feel free to just `#[ignore]` the test, and manually
    /// run the test only when making changes to the logging / tracing setup.
    ///
    /// [`Subscriber`]: tracing::Subscriber
    #[cfg(any(test, feature = "test-utils"))]
    pub fn get_and_insert_test_impl() {
        use tracing::{error_span, info};

        GET_TRACE_ID_FN.get().expect("GET_TRACE_ID_FN not set");
        INSERT_TRACE_ID_FN
            .get()
            .expect("INSERT_TRACE_ID_FN not set");

        let trace_id1 = TraceId::generate();

        // Use error spans so that the test still passes with `RUST_LOG=error`.
        let outer_span = error_span!("(outer)", trace_id=%trace_id1);

        // Sanity check: get_from_span should return no TraceId.
        assert!(TraceId::get_from_span(&outer_span).is_none());

        // Insert the TraceId into the outer span's extensions.
        trace_id1.clone().insert_into_span(&outer_span);

        // Enter the outer span.
        outer_span.in_scope(|| {
            info!("This msg should contain (outer) and `trace_id`");

            // We should be able to recover the trace id within the outer span.
            let current_span = tracing::Span::current();
            let trace_id2 = TraceId::get_from_span(&current_span)
                .expect("No trace id returned");
            assert_eq!(trace_id1, trace_id2);

            // Create an inner span. trace_id should've been set by the parent.
            let inner_span =
                error_span!("(inner)", trace_id = tracing::field::Empty);

            // Enter the inner span.
            inner_span.in_scope(|| {
                info!("This msg should have (outer):(inner) and `trace_id`");

                // Should be able to recover the trace id within the inner span.
                let current_span = tracing::Span::current();
                let trace_id3 = TraceId::get_from_span(&current_span)
                    .expect("No trace id returned");
                assert_eq!(trace_id2, trace_id3);
            });
        });

        info!("Test complete");
    }
}

impl TryFrom<HeaderValue> for TraceId {
    type Error = anyhow::Error;

    fn try_from(src: HeaderValue) -> Result<Self, Self::Error> {
        let src_bytes = src.as_bytes();
        if src_bytes.len() != Self::LENGTH {
            bail!("Source header value had wrong length");
        }

        let all_alphanumeric = src_bytes
            .iter()
            .all(|byte| char::is_alphanumeric(*byte as char));
        ensure!(
            all_alphanumeric,
            "Source header value contained non-alphanumeric bytes"
        );

        Ok(Self(src))
    }
}

impl Display for TraceId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl fmt::Debug for TraceId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{self}")
    }
}

#[cfg(any(test, feature = "test-utils"))]
mod arbitrary_impl {
    use lexe_crypto::rng::FastRng;
    use proptest::{
        arbitrary::{Arbitrary, any},
        strategy::{BoxedStrategy, Strategy},
    };

    use super::*;

    impl Arbitrary for TraceId {
        type Parameters = ();
        type Strategy = BoxedStrategy<Self>;
        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
            any::<FastRng>()
                .prop_map(|mut rng| Self::from_rng(&mut rng))
                .boxed()
        }
    }
}

/// Generates implementations of the functions pointed to by [`GET_TRACE_ID_FN`]
/// and [`INSERT_TRACE_ID_FN`] given the type of the [`tracing::Subscriber`].
/// The caller (typically `lexe_logger::try_init()`) is responsible for
/// initializing these statics using the generated implementations.
///
/// ```ignore
/// # use anyhow::{anyhow, Context};
/// # use tracing_subscriber::{util::SubscriberInitExt, FmtSubscriber};
/// #
/// pub fn try_init() -> anyhow::Result<()> {
///     FmtSubscriber::new().try_init().context("Logger already set")?;
///
///     // Notice how `FmtSubscriber` here exactly matches our subscriber type.
///     // If using a more complex subscriber, you will have to name the type,
///     // e.g. `Layered<Filtered<FmtLayer<Registry, ...>, ..., ...>, ...>`.
///     // See public/logger/src/lib.rs for an example of this.
///     lexe_common::define_trace_id_fns!(FmtSubscriber);
///     lexe_api::trace::GET_TRACE_ID_FN
///         .set(get_trace_id_from_span)
///         .map_err(|_| anyhow!("GET_TRACE_ID_FN already set"))?;
///     lexe_api::trace::INSERT_TRACE_ID_FN
///         .set(insert_trace_id_into_span)
///         .map_err(|_| anyhow!("INSERT_TRACE_ID_FN already set"))?;
///
///     Ok(())
/// }
/// ```
#[macro_export]
macro_rules! define_trace_id_fns {
    ($subscriber:ty) => {
        use anyhow::Context;
        use lexe_api::trace::TraceId;
        use tracing_subscriber::registry::LookupSpan;

        /// Get the [`TraceId`] from the `Extensions` of this span or any of its
        /// parents. Errors if downcasting to the subscriber fails, or if the
        /// subscriber doesn't return a `SpanRef` for the given span id.
        fn get_trace_id_from_span(
            id: &tracing::span::Id,
            dispatch: &tracing::Dispatch,
        ) -> anyhow::Result<Option<TraceId>> {
            let subscriber = dispatch.downcast_ref::<$subscriber>().context(
                "Downcast failed. Did lexe_logger::try_init() define the trace_id \
                 fns with the correct subscriber type?",
            )?;
            let span_ref = subscriber
                .span(id)
                .context("Failed to get SpanRef from id")?;
            let maybe_trace_id = span_ref
                .scope()
                .find_map(|span| span.extensions().get::<TraceId>().cloned());
            Ok(maybe_trace_id)
        }

        /// Insert the [`TraceId`] into the `Extensions` of the given span.
        /// Errors if downcasting to the subscriber fails, or if the
        /// subscriber doesn't return a `SpanRef` for the given span id.
        /// Returns the replaced [`TraceId`] if one already existed in the span.
        fn insert_trace_id_into_span(
            id: &tracing::span::Id,
            dispatch: &tracing::Dispatch,
            trace_id: TraceId,
        ) -> anyhow::Result<Option<TraceId>> {
            let subscriber = dispatch.downcast_ref::<$subscriber>().context(
                "Downcast failed. Did lexe_logger::try_init() define the trace_id \
                 fns with the correct subscriber type?",
            )?;
            let span_ref = subscriber.span(id).context("No span ref for id")?;
            let maybe_replaced = span_ref.extensions_mut().replace(trace_id);
            Ok(maybe_replaced)
        }
    };
}

/// Client tracing utilities.
pub(crate) mod client {
    use tracing::info_span;

    use super::*;

    /// Get a [`tracing::Span`] and [`TraceId`] for a client request.
    pub(crate) fn request_span(
        req: &reqwest::Request,
        from: &str,
        to: &'static str,
    ) -> (tracing::Span, TraceId) {
        // Our client request span.
        let request_span = info_span!(
            target: TARGET,
            "(req)(cli)",
            // This does not overwrite existing values if the field was already
            // set by a parent span. If it was not set, we will record it below.
            trace_id = tracing::field::Empty,
            %from,
            %to,
            method = %req.method(),
            url = %req.url(),
            // This is set later in the request
            attempts_left = tracing::field::Empty,
        );

        // Check if a parent span already has a trace_id set in its Extensions,
        // and extract it if so. This happens when an outgoing client request is
        // made in the API handler of a server which has created a (server-side)
        // request span for a (client) request which included a trace id header.
        let existing_trace_id =
            TraceId::get_from_span(&tracing::Span::current());

        // If we found a TraceId from a parent span, the parent will already
        // have set the trace_id field and included it in its Extensions, so
        // there is nothing to do. Otherwise, we need to generate a new TraceId,
        // record it in the trace_id field, and add it our span's Extensions.
        // NOTE: Setting the trace_id field here after it has already been set
        // by a parent will result in duplicate trace_id keys in logs.
        let trace_id = match existing_trace_id {
            Some(tid) => tid,
            None => {
                let trace_id = TraceId::generate();
                request_span.record("trace_id", trace_id.as_str());

                trace_id.clone().insert_into_span(&request_span);

                trace_id
            }
        };

        (request_span, trace_id)
    }
}

/// Server tracing utilities.
pub(crate) mod server {
    use anyhow::anyhow;
    use http::header::USER_AGENT;
    use tower_http::{
        classify::{
            ClassifiedResponse, ClassifyResponse, NeverClassifyEos,
            SharedClassifier,
        },
        trace::{
            MakeSpan, OnEos, OnFailure, OnRequest, OnResponse, TraceLayer,
        },
    };
    use tracing::{debug, error, info_span, warn};

    use super::*;

    /// Builds a [`TraceLayer`] which:
    ///
    /// - Instruments each incoming request with its own request span, reusing
    ///   the [`TraceId`] from the `lexe-trace-id` header if available
    /// - Logs "New server request" at the start of each received request
    /// - Logs "Done (result) after the completion of each response"
    /// - Logs "Stream ended" when streaming bodies (post-response) complete
    /// - Logs "Other failure" whenever anything else goes wrong
    ///
    /// It can be passed to e.g. [`Router::layer`] or [`ServiceBuilder::layer`].
    ///
    /// [`Router::layer`]: axum::Router::layer
    /// [`ServiceBuilder::layer`]: tower::ServiceBuilder::layer
    pub(crate) fn trace_layer(
        api_span: tracing::Span,
    ) -> TraceLayer<
        SharedClassifier<LxClassifyResponse>,
        LxMakeSpan,
        LxOnRequest,
        LxOnResponse,
        (),
        LxOnEos,
        LxOnFailure,
    > {
        // `tower_http::trace` documents when each of these callbacks is called.
        TraceLayer::new(SharedClassifier::new(LxClassifyResponse))
            .make_span_with(LxMakeSpan { api_span })
            .on_request(LxOnRequest)
            .on_response(LxOnResponse)
            // Do nothing on body chunk
            .on_body_chunk(())
            .on_eos(LxOnEos)
            .on_failure(LxOnFailure)
    }

    /// A [`ClassifyResponse`] which classifies all responses as OK.
    ///
    /// We do this because all responses (including error responses) are already
    /// logged by [`LxOnResponse`], so triggering [`OnFailure`] on error status
    /// codes (the default impl for HTTP) would result in redundant logs.
    #[derive(Clone)]
    pub(crate) struct LxClassifyResponse;

    impl ClassifyResponse for LxClassifyResponse {
        type FailureClass = anyhow::Error;
        type ClassifyEos = NeverClassifyEos<Self::FailureClass>;

        fn classify_response<B>(
            self,
            _response: &http::Response<B>,
        ) -> ClassifiedResponse<Self::FailureClass, Self::ClassifyEos> {
            ClassifiedResponse::Ready(Ok(()))
        }

        fn classify_error<E: Display + 'static>(
            self,
            error: &E,
        ) -> Self::FailureClass {
            anyhow!("{error:#}")
        }
    }

    /// A [`MakeSpan`] impl which mirrors [`client::request_span`].
    #[derive(Clone)]
    pub(crate) struct LxMakeSpan {
        /// The server API span, used as each request span's parent.
        api_span: tracing::Span,
    }

    impl<B> MakeSpan<B> for LxMakeSpan {
        fn make_span(&mut self, request: &http::Request<B>) -> tracing::Span {
            // Get the full url, including query params
            let url = request
                .uri()
                .path_and_query()
                .map(|url| url.as_str())
                .unwrap_or("/");

            // Parse the client-provided trace id from the trace id header.
            // Generate a new trace id if none existed or if the header value
            // was invalid. There should be no need to try to get a TraceId from
            // a parent span's Extensions.
            let trace_id = request
                .headers()
                .get(&TRACE_ID_HEADER_NAME)
                .and_then(|value| TraceId::try_from(value.clone()).ok())
                .unwrap_or_else(TraceId::generate);

            // Log the user agent header as `from` if it exists, or a default.
            // This is the same `from` as in the RestClient and client span.
            // The `to` field does not need to be logged since it is already
            // included as part of the server's span name.
            let from = request
                .headers()
                .get(USER_AGENT)
                .map(|value| value.to_str().unwrap_or("(non-ascii)"))
                .unwrap_or("(none)");

            let request_span = info_span!(
                target: TARGET,
                parent: self.api_span.clone(),
                "(req)(srv)",
                %trace_id,
                %from,
                method = %request.method().as_str(),
                url = %url,
                version = ?request.version(),
            );

            // Insert the trace id into the server request span's `Extensions`,
            // so that any client requests made in our handler can pick it up.
            trace_id.insert_into_span(&request_span);

            request_span
        }
    }

    /// `OnRequest` impl mirroring `RestClient::send_inner`.
    #[derive(Clone)]
    pub(crate) struct LxOnRequest;

    impl<B> OnRequest<B> for LxOnRequest {
        fn on_request(
            &mut self,
            request: &http::Request<B>,
            _request_span: &tracing::Span,
        ) {
            let headers = request.headers();
            debug!(target: TARGET, "New server request");
            debug!(target: TARGET, ?headers, "Server request (headers)");
        }
    }

    /// [`OnResponse`] impl which logs the completion of requests by the server.
    /// `RestClient` logs `req_time`; analogously here we log `resp_time`.
    #[derive(Clone)]
    pub(crate) struct LxOnResponse;

    impl<B> OnResponse<B> for LxOnResponse {
        fn on_response(
            self,
            response: &http::Response<B>,
            // Client logs "req_time", server logs "resp_time"
            resp_time: Duration,
            _request_span: &tracing::Span,
        ) {
            let status = response.status();
            let headers = response.headers();
            let resp_time = DisplayMs(resp_time);

            if status.is_success() {
                // NOTE: This server request log can be at INFO.
                // It's cluttering our logs though, so we're suppressing.
                debug!(target: TARGET, %resp_time, ?status, "Done (success)");
            } else if status.is_client_error() {
                warn!(target: TARGET, %resp_time, ?status, "Done (client error)");
            } else if status.is_server_error() && status.as_u16() == 503 {
                // Don't spam ERRORs for 503 "Service Unavailable"s which we
                // return when load-shedding requests. ERRORs should be serious.
                warn!(target: TARGET, %resp_time, ?status, "Done (load shedded)");
            } else if status.is_server_error() {
                error!(target: TARGET, %resp_time, ?status, "Done (server error)");
            } else {
                // NOTE: This server request log can be at INFO.
                // It's cluttering our logs though, so we're suppressing.
                debug!(target: TARGET, %resp_time, ?status, "Done (other)");
            }

            // Log the headers too, but only at DEBUG.
            debug!(
                target: TARGET, %resp_time, ?status, ?headers,
                "Done (headers)",
            );
        }
    }

    /// Basic [`OnEos`] impl; we don't stream atm but this will work if we do
    #[derive(Clone)]
    pub(crate) struct LxOnEos;

    impl OnEos for LxOnEos {
        fn on_eos(
            self,
            trailers: Option<&http::HeaderMap>,
            // The duration since the response was sent
            stream_time: Duration,
            _request_span: &tracing::Span,
        ) {
            let num_trailers = trailers.map(|trailers| trailers.len());
            let stream_time = DisplayMs(stream_time);
            debug!(target: TARGET, %stream_time, ?num_trailers, "Stream ended");
        }
    }

    /// [`OnFailure`] impl which logs failures. Since [`LxClassifyResponse`]
    /// does not classify any responses (including error responses) or
    /// end-of-streams as failures, this will trigger only if the inner
    /// [`tower::Service`] future resolves to an error, or if `Body::poll_frame`
    /// returns an error. See [`tower_http::trace`] module docs for more info.
    #[derive(Clone)]
    pub(crate) struct LxOnFailure;

    impl<FailureClass: Display> OnFailure<FailureClass> for LxOnFailure {
        fn on_failure(
            &mut self,
            fail_class: FailureClass,
            // The duration since the request was received
            fail_time: Duration,
            _request_span: &tracing::Span,
        ) {
            let fail_time = DisplayMs(fail_time);
            warn!(target: TARGET, %fail_time, %fail_class, "Other failure");
        }
    }
}

#[cfg(test)]
mod test {
    use proptest::{prop_assert_eq, proptest};

    use super::*;

    #[test]
    fn trace_id_proptest() {
        // TraceId's Arbitrary impl uses TraceId::from_rng
        proptest!(|(id1: TraceId)| {
            // Ensure the debug_assert! in TraceId::as_str() doesn't panic
            id1.as_str();
            // TraceId -> HeaderValue -> TraceId
            let id2 = TraceId::try_from(id1.to_header_value()).unwrap();
            prop_assert_eq!(&id1, &id2);
        });
    }
}