Emit structured JSON logs only when stderr is not a TTY
Assisted-by: Claude Opus 4.7 via Claude Code
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 {