Emit structured JSON logs only when stderr is not a TTY
Assisted-by: Claude Opus 4.7 via Claude Code
change urwuwrvknnlvystoznuoouxqvxozpyrn
commit ffa0f1d82966fb833f1d99061e447b84870042f2
author Alpha Chen <alpha@kejadlen.dev>
date
parent znrsoptt
diff --git a/Cargo.lock b/Cargo.lock
index b3dc9e4..bf4aa56 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2902,6 +2902,16 @@ dependencies = [
  "tracing-core",
 ]
 
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
 [[package]]
 name = "tracing-subscriber"
 version = "0.3.23"
@@ -2912,12 +2922,15 @@ dependencies = [
  "nu-ansi-term",
  "once_cell",
  "regex-automata",
+ "serde",
+ "serde_json",
  "sharded-slab",
  "smallvec",
  "thread_local",
  "tracing",
  "tracing-core",
  "tracing-log",
+ "tracing-serde",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 58c1ed5..0a8688e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,7 +28,7 @@ tempfile = "*"
 thiserror = "*"
 tokio = { version = "*", features = ["full"] }
 tracing = "*"
-tracing-subscriber = { version = "*", features = ["env-filter"] }
+tracing-subscriber = { version = "*", features = ["env-filter", "json"] }
 uuid = { version = "*", features = ["v7"] }
 walkdir = "*"
 
diff --git a/src/bin/quire/main.rs b/src/bin/quire/main.rs
index cd3de71..6688950 100644
--- a/src/bin/quire/main.rs
+++ b/src/bin/quire/main.rs
@@ -6,7 +6,9 @@ use miette::IntoDiagnostic;
 use miette::Result;
 use quire::Quire;
 use sentry::ClientInitGuard;
+use std::io::IsTerminal;
 use tracing_subscriber::EnvFilter;
+use tracing_subscriber::Layer;
 use tracing_subscriber::fmt;
 use tracing_subscriber::prelude::*;
 
@@ -126,22 +128,38 @@ fn init_sentry(quire: &Quire) -> Option<ClientInitGuard> {
     Some(guard)
 }
 
-#[tokio::main]
-async fn main() -> Result<()> {
-    let quire = Quire::default();
-    let _sentry = init_sentry(&quire);
+/// Initialize tracing with a stderr fmt layer.
+///
+/// Emits structured JSON when stderr is not a terminal (e.g. piped to a log
+/// collector), and human-readable text when running interactively.
+fn init_tracing() -> Result<()> {
+    let filter = EnvFilter::builder()
+        .with_env_var("QUIRE_LOG")
+        .from_env()
+        .into_diagnostic()?;
+
+    let layer = fmt::layer().with_writer(std::io::stderr);
+    let fmt_layer = if std::io::stderr().is_terminal() {
+        layer.boxed()
+    } else {
+        layer.json().boxed()
+    };
 
     tracing_subscriber::registry()
         .with(sentry_tracing::layer())
-        .with(fmt::layer().with_writer(std::io::stderr))
-        .with(
-            EnvFilter::builder()
-                .with_env_var("QUIRE_LOG")
-                .from_env()
-                .into_diagnostic()?,
-        )
+        .with(fmt_layer)
+        .with(filter)
         .init();
 
+    Ok(())
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    let quire = Quire::default();
+    let _sentry = init_sentry(&quire);
+    init_tracing()?;
+
     let cli = Cli::parse();
 
     if let Some(shell) = cli.completions {