diff --git a/libs/observability/src/msg_json_fmt.rs b/libs/observability/src/msg_json_fmt.rs index ae0691f..1b98a0f 100644 --- a/libs/observability/src/msg_json_fmt.rs +++ b/libs/observability/src/msg_json_fmt.rs @@ -156,7 +156,13 @@ where ordered.insert(key.clone(), value.clone()); } - write!(writer, "{}", serde_json::to_string(&ordered).map_err(|_| fmt::Error)?) + // Use pretty JSON for human readability in terminals, compact for pipelines + let json = if std::env::var("APP_LOG_PRETTY").as_deref() == Ok("true") { + serde_json::to_string_pretty(&ordered) + } else { + serde_json::to_string(&ordered) + }; + write!(writer, "{}", json.map_err(|_| fmt::Error)?) } } diff --git a/libs/observability/src/tracing_fmt.rs b/libs/observability/src/tracing_fmt.rs index 230e640..fd2e934 100644 --- a/libs/observability/src/tracing_fmt.rs +++ b/libs/observability/src/tracing_fmt.rs @@ -1,18 +1,21 @@ -//! Tracing subscriber initialisation with JSON output. +//! Tracing subscriber initialisation. //! -//! Uses a custom `MsgJsonFormat` that injects `_msg` as the first field -//! for VictoriaLogs compatibility. +//! Terminal (TTY): human-readable format with colors +//! Pipeline (non-TTY): JSON output for VictoriaLogs +//! Override via `APP_LOG_FORMAT=json|pretty` env var. use crate::msg_json_fmt::MsgJsonFormat; use once_cell::sync::Lazy; +use std::io::IsTerminal; use std::str::FromStr; use tracing_subscriber::{ - fmt::{self, format::FmtSpan, Layer as FmtLayer}, + fmt::{self, format::FmtSpan}, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, }; +use tracing_subscriber::Layer; /// Global instance identifier, resolved once at startup. /// Priority: `INSTANCE_ID` env var → system hostname → `"unknown"`. @@ -34,23 +37,42 @@ pub fn instance_id() -> String { INSTANCE_ID.clone() } -/// Initialises the global tracing subscriber with JSON-formatted output to stderr. +/// Determines the log format based on environment and TTY detection. +fn use_json() -> bool { + match std::env::var("APP_LOG_FORMAT").as_deref() { + Ok("json") => true, + Ok("pretty") => false, + _ => !std::io::stdout().is_terminal(), // TTY → pretty, non-TTY → json + } +} + +/// Initialises the global tracing subscriber. /// -/// Each JSON line includes `_msg` (first field), `timestamp`, `level`, `target`, -/// `file`, `line`, and structured event fields. +/// TTY terminals get human-readable output with colors. +/// Non-TTY (pipes, container logs) get JSON for log aggregation. +/// `APP_LOG_FORMAT=json|pretty` overrides auto-detection. /// `RUST_LOG` env var controls the log level filter. /// -/// Pass `defer = true` when OTLP will be initialized afterwards via `init_otlp()`; -/// in that case this function only builds the subscriber without calling `try_init()`, -/// and the combined (fmt + OTLP) subscriber is installed by `init_otlp()` instead. +/// Pass `defer = true` when OTLP will be initialized afterwards via `init_otlp()`. pub fn init_tracing_subscriber(level: &str, defer: bool) { let env_filter = EnvFilter::try_from_default_env() .or_else(|_| EnvFilter::from_str(level)) .expect("invalid log level"); - let mut fmt_layer: FmtLayer<_, _, _, _> = fmt::layer() - .event_format(MsgJsonFormat); - fmt_layer.set_span_events(FmtSpan::CLOSE); + let fmt_layer: Box + Send + Sync> = if use_json() { + let mut layer = fmt::layer() + .event_format(MsgJsonFormat); + layer.set_span_events(FmtSpan::CLOSE); + <_ as Layer<_>>::boxed(layer) + } else { + let mut layer = fmt::layer() + .with_target(false) + .with_level(true) + .with_ansi(std::io::stdout().is_terminal()) + .pretty(); + layer.set_span_events(FmtSpan::CLOSE); + <_ as Layer<_>>::boxed(layer) + }; let registry = tracing_subscriber::registry() .with(env_filter)