feat(observability): use human-readable log format for terminals
Some checks are pending
CI / Frontend Build (push) Blocked by required conditions
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run

When stdout is connected to a TTY, use tracing_subscriber's pretty
format with colors instead of single-line JSON. Non-TTY (container
logs, pipes) continue to output JSON for log aggregation.

Override auto-detection via APP_LOG_FORMAT=json|pretty.

Also adds APP_LOG_PRETTY=true to use serde_json::to_string_pretty
for human-readable JSON output (useful for development/debugging).
This commit is contained in:
ZhenYi 2026-04-26 16:39:03 +08:00
parent ecf9f33b26
commit 7d7103e271
2 changed files with 42 additions and 14 deletions

View File

@ -156,7 +156,13 @@ where
ordered.insert(key.clone(), value.clone()); 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)?)
} }
} }

View File

@ -1,18 +1,21 @@
//! Tracing subscriber initialisation with JSON output. //! Tracing subscriber initialisation.
//! //!
//! Uses a custom `MsgJsonFormat` that injects `_msg` as the first field //! Terminal (TTY): human-readable format with colors
//! for VictoriaLogs compatibility. //! Pipeline (non-TTY): JSON output for VictoriaLogs
//! Override via `APP_LOG_FORMAT=json|pretty` env var.
use crate::msg_json_fmt::MsgJsonFormat; use crate::msg_json_fmt::MsgJsonFormat;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::io::IsTerminal;
use std::str::FromStr; use std::str::FromStr;
use tracing_subscriber::{ use tracing_subscriber::{
fmt::{self, format::FmtSpan, Layer as FmtLayer}, fmt::{self, format::FmtSpan},
layer::SubscriberExt, layer::SubscriberExt,
util::SubscriberInitExt, util::SubscriberInitExt,
EnvFilter, EnvFilter,
}; };
use tracing_subscriber::Layer;
/// Global instance identifier, resolved once at startup. /// Global instance identifier, resolved once at startup.
/// Priority: `INSTANCE_ID` env var → system hostname → `"unknown"`. /// Priority: `INSTANCE_ID` env var → system hostname → `"unknown"`.
@ -34,23 +37,42 @@ pub fn instance_id() -> String {
INSTANCE_ID.clone() 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`, /// TTY terminals get human-readable output with colors.
/// `file`, `line`, and structured event fields. /// 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. /// `RUST_LOG` env var controls the log level filter.
/// ///
/// Pass `defer = true` when OTLP will be initialized afterwards via `init_otlp()`; /// 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.
pub fn init_tracing_subscriber(level: &str, defer: bool) { pub fn init_tracing_subscriber(level: &str, defer: bool) {
let env_filter = EnvFilter::try_from_default_env() let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::from_str(level)) .or_else(|_| EnvFilter::from_str(level))
.expect("invalid log level"); .expect("invalid log level");
let mut fmt_layer: FmtLayer<_, _, _, _> = fmt::layer() let fmt_layer: Box<dyn Layer<_> + Send + Sync> = if use_json() {
.event_format(MsgJsonFormat); let mut layer = fmt::layer()
fmt_layer.set_span_events(FmtSpan::CLOSE); .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() let registry = tracing_subscriber::registry()
.with(env_filter) .with(env_filter)