Skip to content

Commit 35a5c61

Browse files
committed
feat: impove log output formatting
1 parent 29025ca commit 35a5c61

File tree

2 files changed

+273
-49
lines changed

2 files changed

+273
-49
lines changed

crates/inferadb-management-core/src/logging.rs

Lines changed: 258 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,178 @@
1-
use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt};
1+
//! Structured logging utilities for InferaDB Management
2+
//!
3+
//! Provides enhanced logging with contextual fields and formatting options,
4+
//! matching the server's logging architecture for consistent developer experience.
5+
6+
use std::io::IsTerminal;
7+
8+
use tracing_subscriber::{
9+
EnvFilter, Layer, fmt, fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt,
10+
};
211

312
use crate::config::ObservabilityConfig;
413

5-
/// Initialize structured logging based on configuration
14+
/// Log output format options
15+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16+
pub enum LogFormat {
17+
/// Standard single-line format (matches server default)
18+
/// Output: `2025-01-15T10:30:45.123456Z INFO target: message key=value`
19+
Full,
20+
/// Human-readable multi-line format with colors (for development debugging)
21+
Pretty,
22+
/// Compact single-line format without timestamp details
23+
Compact,
24+
/// JSON format (for production log aggregation)
25+
Json,
26+
}
27+
28+
#[allow(clippy::derivable_impls)]
29+
impl Default for LogFormat {
30+
fn default() -> Self {
31+
#[cfg(debug_assertions)]
32+
{
33+
LogFormat::Full // Match server's default format in development
34+
}
35+
#[cfg(not(debug_assertions))]
36+
{
37+
LogFormat::Json
38+
}
39+
}
40+
}
41+
42+
/// Configuration for logging behavior
43+
#[derive(Debug, Clone)]
44+
pub struct LogConfig {
45+
/// Output format
46+
pub format: LogFormat,
47+
/// Whether to include file/line numbers
48+
pub include_location: bool,
49+
/// Whether to include target module
50+
pub include_target: bool,
51+
/// Whether to include thread IDs
52+
pub include_thread_id: bool,
53+
/// Whether to log span events (enter/exit/close)
54+
pub log_spans: bool,
55+
/// Whether to use ANSI colors (None = auto-detect based on TTY)
56+
pub ansi: Option<bool>,
57+
/// Environment filter (e.g., "info,inferadb_management=debug")
58+
pub filter: Option<String>,
59+
}
60+
61+
impl Default for LogConfig {
62+
fn default() -> Self {
63+
Self {
64+
format: LogFormat::default(),
65+
include_location: cfg!(debug_assertions),
66+
include_target: true,
67+
include_thread_id: false,
68+
log_spans: cfg!(debug_assertions),
69+
ansi: None, // Auto-detect
70+
filter: None,
71+
}
72+
}
73+
}
74+
75+
/// Initialize structured logging with configuration
76+
///
77+
/// This is the primary logging initialization function that provides full control
78+
/// over log format and behavior, matching the server's logging API.
79+
///
80+
/// # Arguments
81+
///
82+
/// * `config` - Logging configuration options
83+
///
84+
/// # Examples
85+
///
86+
/// ```no_run
87+
/// use inferadb_management_core::logging::{LogConfig, LogFormat, init_logging};
88+
///
89+
/// // Development: Pretty format with colors
90+
/// let config = LogConfig {
91+
/// format: LogFormat::Pretty,
92+
/// ..Default::default()
93+
/// };
94+
/// init_logging(config).unwrap();
95+
///
96+
/// // Production: JSON format
97+
/// let config = LogConfig {
98+
/// format: LogFormat::Json,
99+
/// filter: Some("info".to_string()),
100+
/// ..Default::default()
101+
/// };
102+
/// init_logging(config).unwrap();
103+
/// ```
104+
pub fn init_logging(config: LogConfig) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
105+
let env_filter = if let Some(filter) = &config.filter {
106+
EnvFilter::try_new(filter)?
107+
} else {
108+
EnvFilter::try_from_default_env()
109+
.unwrap_or_else(|_| EnvFilter::new("info,inferadb_management=debug"))
110+
};
111+
112+
// Auto-detect ANSI support based on TTY, or use explicit setting
113+
let ansi = config.ansi.unwrap_or_else(|| std::io::stdout().is_terminal());
114+
115+
let fmt_span = if config.log_spans { FmtSpan::NEW | FmtSpan::CLOSE } else { FmtSpan::NONE };
116+
117+
match config.format {
118+
LogFormat::Full => {
119+
// Standard format matching server's default output style
120+
let fmt_layer = fmt::layer().with_target(config.include_target).with_filter(env_filter);
121+
122+
tracing_subscriber::registry().with(fmt_layer).try_init()?;
123+
},
124+
LogFormat::Pretty => {
125+
let fmt_layer = fmt::layer()
126+
.pretty()
127+
.with_ansi(ansi)
128+
.with_target(config.include_target)
129+
.with_thread_ids(config.include_thread_id)
130+
.with_file(config.include_location)
131+
.with_line_number(config.include_location)
132+
.with_span_events(fmt_span)
133+
.with_filter(env_filter);
134+
135+
tracing_subscriber::registry().with(fmt_layer).try_init()?;
136+
},
137+
LogFormat::Compact => {
138+
let fmt_layer = fmt::layer()
139+
.compact()
140+
.with_ansi(ansi)
141+
.with_target(config.include_target)
142+
.with_thread_ids(config.include_thread_id)
143+
.with_file(config.include_location)
144+
.with_line_number(config.include_location)
145+
.with_span_events(fmt_span)
146+
.with_filter(env_filter);
147+
148+
tracing_subscriber::registry().with(fmt_layer).try_init()?;
149+
},
150+
LogFormat::Json => {
151+
let fmt_layer = fmt::layer()
152+
.json()
153+
.with_target(config.include_target)
154+
.with_current_span(true)
155+
.with_span_list(true)
156+
.with_thread_ids(config.include_thread_id)
157+
.with_thread_names(config.include_thread_id)
158+
.with_filter(env_filter);
159+
160+
tracing_subscriber::registry().with(fmt_layer).try_init()?;
161+
},
162+
}
163+
164+
tracing::debug!(
165+
format = ?config.format,
166+
location = config.include_location,
167+
target = config.include_target,
168+
ansi = ansi,
169+
"Logging initialized"
170+
);
171+
172+
Ok(())
173+
}
174+
175+
/// Initialize structured logging based on ObservabilityConfig (backward compatible)
6176
///
7177
/// Sets up tracing-subscriber with either JSON or compact formatting based on environment.
8178
/// In production (when `json` is true), logs are emitted as JSON for structured ingestion.
@@ -32,34 +202,18 @@ use crate::config::ObservabilityConfig;
32202
/// logging::init(&config, false);
33203
/// ```
34204
pub fn init(config: &ObservabilityConfig, json: bool) {
35-
let env_filter = EnvFilter::try_from_default_env()
36-
.or_else(|_| EnvFilter::try_new(&config.log_level))
37-
.unwrap_or_else(|_| EnvFilter::new("info"));
38-
39-
if json {
40-
// Production: JSON structured logging
41-
let fmt_layer = fmt::layer()
42-
.json()
43-
.with_target(true)
44-
.with_current_span(true)
45-
.with_span_list(true)
46-
.with_thread_ids(true)
47-
.with_thread_names(true)
48-
.with_filter(env_filter);
49-
50-
tracing_subscriber::registry().with(fmt_layer).init();
51-
} else {
52-
// Development: Compact single-line logging (matches server format)
53-
let fmt_layer = fmt::layer()
54-
.compact()
55-
.with_target(true)
56-
.with_thread_ids(false)
57-
.with_thread_names(false)
58-
.with_file(false)
59-
.with_line_number(false)
60-
.with_filter(env_filter);
205+
let log_config = LogConfig {
206+
format: if json { LogFormat::Json } else { LogFormat::Full },
207+
filter: Some(config.log_level.clone()),
208+
include_location: false,
209+
include_target: true,
210+
include_thread_id: json, // Include thread info in JSON mode
211+
log_spans: false,
212+
ansi: None, // Auto-detect
213+
};
61214

62-
tracing_subscriber::registry().with(fmt_layer).init();
215+
if let Err(e) = init_logging(log_config) {
216+
eprintln!("Failed to initialize logging: {}", e);
63217
}
64218
}
65219

@@ -89,7 +243,7 @@ pub fn init_with_tracing(
89243

90244
let env_filter = EnvFilter::try_from_default_env()
91245
.or_else(|_| EnvFilter::try_new(&config.log_level))
92-
.unwrap_or_else(|_| EnvFilter::new("info"));
246+
.unwrap_or_else(|_| EnvFilter::new("info,inferadb_management=debug"));
93247

94248
// Build the base logging layer
95249
let fmt_layer = if json {
@@ -104,16 +258,8 @@ pub fn init_with_tracing(
104258
.with_filter(env_filter.clone())
105259
.boxed()
106260
} else {
107-
// Development: Compact single-line logging (matches server format)
108-
fmt::layer()
109-
.compact()
110-
.with_target(true)
111-
.with_thread_ids(false)
112-
.with_thread_names(false)
113-
.with_file(false)
114-
.with_line_number(false)
115-
.with_filter(env_filter.clone())
116-
.boxed()
261+
// Development: Standard format (matches server default)
262+
fmt::layer().with_target(true).with_filter(env_filter.clone()).boxed()
117263
};
118264

119265
let subscriber = tracing_subscriber::registry().with(fmt_layer);
@@ -164,14 +310,76 @@ pub fn init_with_tracing(
164310

165311
#[cfg(test)]
166312
mod tests {
313+
use std::sync::Once;
314+
167315
use super::*;
168316

169-
// Note: We cannot test init() directly in unit tests because
170-
// tracing-subscriber only allows setting the global default subscriber once per process.
171-
// The logging initialization is tested through integration tests.
317+
static INIT: Once = Once::new();
318+
319+
fn init_test_logging() {
320+
INIT.call_once(|| {
321+
let _ = init_logging(LogConfig {
322+
format: LogFormat::Compact,
323+
include_location: false,
324+
include_target: false,
325+
include_thread_id: false,
326+
log_spans: true,
327+
ansi: Some(false),
328+
filter: Some("debug".to_string()),
329+
});
330+
});
331+
}
332+
333+
#[test]
334+
fn test_log_config_default() {
335+
let config = LogConfig::default();
336+
assert_eq!(config.format, LogFormat::default());
337+
assert!(config.include_target);
338+
assert!(!config.include_thread_id);
339+
assert!(config.ansi.is_none()); // Auto-detect
340+
}
172341

173342
#[test]
174-
fn test_config_creation() {
343+
fn test_log_format_default() {
344+
let format = LogFormat::default();
345+
#[cfg(debug_assertions)]
346+
assert_eq!(format, LogFormat::Full);
347+
#[cfg(not(debug_assertions))]
348+
assert_eq!(format, LogFormat::Json);
349+
}
350+
351+
#[test]
352+
fn test_log_format_variants() {
353+
assert_eq!(LogFormat::Full, LogFormat::Full);
354+
assert_eq!(LogFormat::Pretty, LogFormat::Pretty);
355+
assert_eq!(LogFormat::Compact, LogFormat::Compact);
356+
assert_eq!(LogFormat::Json, LogFormat::Json);
357+
assert_ne!(LogFormat::Full, LogFormat::Json);
358+
}
359+
360+
#[test]
361+
fn test_log_config_custom() {
362+
let config = LogConfig {
363+
format: LogFormat::Json,
364+
include_location: true,
365+
include_target: false,
366+
include_thread_id: true,
367+
log_spans: true,
368+
ansi: Some(false),
369+
filter: Some("warn".to_string()),
370+
};
371+
372+
assert_eq!(config.format, LogFormat::Json);
373+
assert!(config.include_location);
374+
assert!(!config.include_target);
375+
assert!(config.include_thread_id);
376+
assert!(config.log_spans);
377+
assert_eq!(config.ansi, Some(false));
378+
assert_eq!(config.filter, Some("warn".to_string()));
379+
}
380+
381+
#[test]
382+
fn test_observability_config_creation() {
175383
let config = ObservabilityConfig {
176384
log_level: "debug".to_string(),
177385
metrics_enabled: true,
@@ -184,6 +392,12 @@ mod tests {
184392
assert!(!config.tracing_enabled);
185393
}
186394

395+
#[test]
396+
fn test_init_logging_does_not_panic() {
397+
init_test_logging();
398+
// If we get here without panicking, the test passes
399+
}
400+
187401
#[cfg(feature = "opentelemetry")]
188402
#[test]
189403
fn test_init_with_tracing_disabled() {

crates/inferadb-management/src/main.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,22 @@ async fn main() -> Result<()> {
3939
let config = ManagementConfig::load(&args.config)?;
4040
config.validate()?;
4141

42-
// Determine if we should use JSON logging
43-
// Use JSON in production or when explicitly requested
44-
let use_json = args.json_logs || args.environment == "production";
42+
// Initialize structured logging with environment-appropriate format
43+
// Use Full format (matching server) in development, JSON in production
44+
let log_config = logging::LogConfig {
45+
format: if args.json_logs || args.environment == "production" {
46+
logging::LogFormat::Json
47+
} else {
48+
logging::LogFormat::Full // Match server's default output style
49+
},
50+
filter: Some(config.observability.log_level.clone()),
51+
..Default::default()
52+
};
4553

46-
// Initialize structured logging
47-
logging::init(&config.observability, use_json);
54+
if let Err(e) = logging::init_logging(log_config) {
55+
eprintln!("Failed to initialize logging: {}", e);
56+
std::process::exit(1);
57+
}
4858

4959
tracing::info!(
5060
version = env!("CARGO_PKG_VERSION"),

0 commit comments

Comments
 (0)