Register MietteLayer before sentry-tracing in subscriber chain
tracing_subscriber::Layered::on_event dispatches inner-first, so the
layer added earlier via .with() fires first. The previous ordering put
sentry_tracing inner and miette outer, which meant capture_event ran
(invoking before_send synchronously) before MietteLayer had a chance to
populate the thread-local — so the rendered diagnostic was never
attached to extra["diagnostic"] and Sentry events arrived with only the
short Display message.
Swap the order in both binaries, correct the layer-ordering docstring,
and add a regression test that installs a downstream capture layer to
verify the thread-local is set by the time a subsequent on_event reads
it.
Assisted-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
diff --git a/quire-ci/src/main.rs b/quire-ci/src/main.rs
index a34e686..b8d4201 100644
--- a/quire-ci/src/main.rs
+++ b/quire-ci/src/main.rs
@@ -279,8 +279,8 @@ fn init_tracing(miette_layer: quire_core::telemetry::MietteLayer) -> miette::Res
.into_diagnostic()?;
tracing_subscriber::registry()
- .with(sentry_tracing::layer())
.with(miette_layer)
+ .with(sentry_tracing::layer())
.with(fmt::layer().with_writer(std::io::stderr))
.with(filter)
.init();
diff --git a/quire-core/src/telemetry.rs b/quire-core/src/telemetry.rs
index c2c1618..2b73fa4 100644
--- a/quire-core/src/telemetry.rs
+++ b/quire-core/src/telemetry.rs
@@ -19,14 +19,16 @@ type RenderFn = Box<dyn (Fn(&(dyn Error + 'static)) -> Option<String>) + Send +
///
/// # Layer ordering
///
-/// Add this layer **after** `sentry_tracing::layer()` in the `.with()` chain
-/// so it fires first and sets the thread-local before sentry-tracing's
-/// `on_event` calls `capture_event` (which invokes `before_send` synchronously).
+/// Register this layer **before** `sentry_tracing::layer()` in the `.with()`
+/// chain. `tracing_subscriber::Layered::on_event` dispatches inner-first, so
+/// the layer added earlier fires first — and this layer must set the
+/// thread-local before sentry-tracing's `on_event` calls `capture_event`
+/// (which invokes `before_send` synchronously).
///
/// ```ignore
/// tracing_subscriber::registry()
-/// .with(sentry_tracing::layer())
-/// .with(miette_layer) // fires first — sets thread-local
+/// .with(miette_layer) // fires first — sets thread-local
+/// .with(sentry_tracing::layer()) // fires next — capture_event reads it
/// .with(fmt_layer)
/// .with(filter)
/// .init();
@@ -143,3 +145,70 @@ pub fn before_send(
}
Some(event)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::sync::{Arc, Mutex};
+ use tracing_subscriber::layer::SubscriberExt;
+
+ #[derive(Debug, thiserror::Error, miette::Diagnostic)]
+ #[error("outer message")]
+ struct TestDiag {
+ #[help]
+ help: String,
+ }
+
+ /// Reads the thread-local in its `on_event` to simulate what
+ /// sentry-tracing's layer does (capture_event → before_send reads the
+ /// thread-local). Lets us verify that MietteLayer fires *before* this
+ /// layer in the chain.
+ struct CaptureLayer {
+ seen: Arc<Mutex<Option<String>>>,
+ }
+
+ impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for CaptureLayer {
+ fn on_event(
+ &self,
+ _event: &tracing::Event<'_>,
+ _ctx: tracing_subscriber::layer::Context<'_, S>,
+ ) {
+ MIETTE_RENDER.with(|cell| {
+ if let Some(v) = cell.borrow().as_ref() {
+ *self.seen.lock().unwrap() = Some(v.clone());
+ }
+ });
+ }
+ }
+
+ /// Regression test: tracing_subscriber dispatches `on_event` inner-first,
+ /// so the layer registered *earlier* via `.with()` fires first. The
+ /// MietteLayer must be registered before `sentry_tracing::layer()` so its
+ /// thread-local is populated by the time sentry-tracing's `on_event`
+ /// calls `capture_event` (and thus `before_send`).
+ #[test]
+ fn miette_layer_fires_before_downstream_layers() {
+ let seen = Arc::new(Mutex::new(None));
+ let capture = CaptureLayer { seen: seen.clone() };
+ let miette = MietteLayer::new().with_type::<TestDiag>();
+
+ let subscriber = tracing_subscriber::registry().with(miette).with(capture);
+
+ tracing::subscriber::with_default(subscriber, || {
+ let err = TestDiag {
+ help: "try again".into(),
+ };
+ tracing::error!(error = &err as &(dyn std::error::Error + 'static), "failed",);
+ });
+
+ let rendered = seen
+ .lock()
+ .unwrap()
+ .clone()
+ .expect("downstream layer should have observed the rendered diagnostic");
+ assert!(
+ rendered.contains("outer message"),
+ "rendering should include the diagnostic message: {rendered}"
+ );
+ }
+}
diff --git a/quire-server/src/bin/quire/main.rs b/quire-server/src/bin/quire/main.rs
index ddd0964..6a3aa7f 100644
--- a/quire-server/src/bin/quire/main.rs
+++ b/quire-server/src/bin/quire/main.rs
@@ -163,8 +163,8 @@ fn init_tracing(miette_layer: quire_core::telemetry::MietteLayer) -> Result<()>
};
tracing_subscriber::registry()
- .with(sentry_tracing::layer())
.with(miette_layer)
+ .with(sentry_tracing::layer())
.with(fmt_layer)
.with(filter)
.init();