use std::sync::{Mutex, OnceLock};
use provide_telemetry::{
bind_context, configure_logging, enable_console_capture_for_tests,
enable_json_capture_for_tests, get_logger, reset_logging_config_for_tests,
set_as_global_logger, take_console_capture, take_json_capture, trace, Logger, LoggingConfig,
};
static LOGGER_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
fn logger_lock() -> &'static Mutex<()> {
LOGGER_LOCK.get_or_init(|| Mutex::new(()))
}
#[test]
fn logger_test_logging_works_without_otel() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
configure_logging(LoggingConfig {
level: "TRACE".to_string(),
..LoggingConfig::default()
});
let logger = get_logger(Some("tests.logger"));
logger.info("logger.test.info");
logger.debug("logger.test.debug");
logger.error("logger.test.error");
let events = Logger::drain_events_for_tests();
reset_logging_config_for_tests();
assert_eq!(events.len(), 3);
assert_eq!(events[0].target, "tests.logger");
assert_eq!(events[0].level, "INFO");
assert_eq!(events[0].message, "logger.test.info");
assert_eq!(events[2].level, "ERROR");
}
#[test]
fn logger_test_trace_wrapper_works_without_otel() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
let observed = std::sync::Mutex::new((None::<String>, None::<String>));
let result = trace("tests.trace.wrapper", || {
let trace_context = provide_telemetry::get_trace_context();
*observed.lock().expect("observed lock poisoned") = (
trace_context
.get("trace_id")
.and_then(std::clone::Clone::clone),
trace_context
.get("span_id")
.and_then(std::clone::Clone::clone),
);
41 + 1
});
let trace_context = provide_telemetry::get_trace_context();
let observed = observed.lock().expect("observed lock poisoned");
assert_eq!(result, 42);
assert_eq!(observed.0.as_ref().map(std::string::String::len), Some(32));
assert_eq!(observed.1.as_ref().map(std::string::String::len), Some(16));
assert_eq!(trace_context.get("trace_id"), Some(&None));
assert_eq!(trace_context.get("span_id"), Some(&None));
}
#[test]
fn logger_test_json_emit_produces_canonical_fields() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
std::env::set_var("PROVIDE_LOG_FORMAT", "json");
std::env::set_var("PROVIDE_LOG_INCLUDE_TIMESTAMP", "false");
enable_json_capture_for_tests();
let logger = get_logger(Some("tests.json_emit"));
logger.info("log.output.parity");
let raw = take_json_capture();
std::env::remove_var("PROVIDE_LOG_FORMAT");
std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
let line = String::from_utf8(raw).expect("utf8");
let line = line.trim();
assert!(!line.is_empty(), "expected a JSON line in capture buffer");
let parsed: serde_json::Value = serde_json::from_str(line).expect("valid JSON");
assert_eq!(
parsed["message"], "log.output.parity",
"message field must match"
);
assert_eq!(parsed["level"], "INFO", "level must be uppercase INFO");
assert_eq!(
parsed["logger_name"], "tests.json_emit",
"logger_name must match target"
);
assert!(
parsed.get("timestamp").is_none(),
"timestamp must be absent when disabled"
);
Logger::drain_events_for_tests();
}
#[test]
fn logger_test_json_emit_includes_timestamp_by_default() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
std::env::set_var("PROVIDE_LOG_FORMAT", "json");
std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
enable_json_capture_for_tests();
let logger = get_logger(Some("tests.ts"));
logger.warn("log.timestamp.check");
let raw = take_json_capture();
std::env::remove_var("PROVIDE_LOG_FORMAT");
let line = String::from_utf8(raw).expect("utf8");
let parsed: serde_json::Value = serde_json::from_str(line.trim()).expect("valid JSON");
assert_eq!(parsed["level"], "WARN");
let ts = parsed["timestamp"]
.as_str()
.expect("timestamp must be a string");
assert!(
ts.len() == 24 && ts.ends_with('Z') && ts.contains('T'),
"timestamp {ts:?} must match ISO 8601"
);
Logger::drain_events_for_tests();
}
#[test]
fn logger_test_no_json_emit_in_console_format() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
std::env::set_var("PROVIDE_LOG_FORMAT", "console");
enable_json_capture_for_tests();
let logger = get_logger(Some("tests.console"));
logger.debug("should.not.emit.json");
let raw = take_json_capture();
std::env::remove_var("PROVIDE_LOG_FORMAT");
assert!(
raw.is_empty(),
"no JSON should be captured in console format"
);
Logger::drain_events_for_tests();
}
#[test]
fn logger_test_console_format_writes_readable_line() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
std::env::set_var("PROVIDE_LOG_FORMAT", "console");
std::env::set_var("PROVIDE_LOG_INCLUDE_TIMESTAMP", "false");
enable_console_capture_for_tests();
let logger = get_logger(Some("tests.console_output"));
logger.warn("console.parity.check");
let raw = take_console_capture();
std::env::remove_var("PROVIDE_LOG_FORMAT");
std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
Logger::drain_events_for_tests();
let line = String::from_utf8(raw).expect("utf8");
let line = line.trim();
assert!(!line.is_empty(), "expected console output");
assert!(line.contains("WARN"), "line must contain level: {line}");
assert!(
line.contains("console.parity.check"),
"line must contain message: {line}"
);
assert!(
line.contains("tests.console_output"),
"line must contain target: {line}"
);
assert!(
!line.starts_with("20"),
"timestamp must be absent when disabled: {line}"
);
}
#[test]
fn logger_test_configure_logging_overrides_env() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
std::env::set_var("PROVIDE_LOG_FORMAT", "json");
let cfg = provide_telemetry::LoggingConfig {
fmt: "console".to_string(),
include_timestamp: false,
..provide_telemetry::LoggingConfig::default()
};
configure_logging(cfg);
enable_json_capture_for_tests();
enable_console_capture_for_tests();
let logger = get_logger(Some("tests.configure"));
logger.info("configure.override.check");
let json_raw = take_json_capture();
let console_raw = take_console_capture();
std::env::remove_var("PROVIDE_LOG_FORMAT");
reset_logging_config_for_tests();
Logger::drain_events_for_tests();
assert!(
json_raw.is_empty(),
"override to console must suppress JSON emit"
);
assert!(
!console_raw.is_empty(),
"override to console must produce console output"
);
}
#[test]
fn logger_test_log_trait_routes_to_events() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
let _ = set_as_global_logger();
enable_json_capture_for_tests();
std::env::set_var("PROVIDE_LOG_FORMAT", "json");
std::env::set_var("PROVIDE_LOG_INCLUDE_TIMESTAMP", "false");
log::info!(target: "tests.log_trait", "log.trait.parity");
let raw = take_json_capture();
std::env::remove_var("PROVIDE_LOG_FORMAT");
std::env::remove_var("PROVIDE_LOG_INCLUDE_TIMESTAMP");
Logger::drain_events_for_tests();
let line = String::from_utf8(raw).expect("utf8");
let line = line.trim();
assert!(!line.is_empty(), "log::info! must produce JSON output");
let parsed: serde_json::Value = serde_json::from_str(line).expect("valid JSON");
assert_eq!(parsed["message"], "log.trait.parity");
assert_eq!(parsed["level"], "INFO");
assert_eq!(parsed["logger_name"], "tests.log_trait");
}
#[test]
fn logger_test_log_trait_respects_level_filter() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
let _ = set_as_global_logger();
let cfg = provide_telemetry::LoggingConfig {
level: "INFO".to_string(),
fmt: "json".to_string(),
include_timestamp: false,
..provide_telemetry::LoggingConfig::default()
};
configure_logging(cfg);
enable_json_capture_for_tests();
log::debug!(target: "tests.log_filter", "should.be.filtered");
log::info!(target: "tests.log_filter", "should.pass");
let raw = take_json_capture();
reset_logging_config_for_tests();
Logger::drain_events_for_tests();
let output = String::from_utf8(raw).expect("utf8");
assert!(
!output.contains("should.be.filtered"),
"DEBUG must be filtered at INFO level"
);
assert!(
output.contains("should.pass"),
"INFO must pass through at INFO level"
);
}
#[test]
fn logger_test_log_trait_respects_module_level_override() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
let _ = set_as_global_logger();
let cfg = LoggingConfig {
level: "INFO".to_string(),
fmt: "json".to_string(),
include_timestamp: false,
module_levels: {
let mut m = std::collections::HashMap::new();
m.insert("tests.mod_override".to_string(), "DEBUG".to_string());
m
},
..LoggingConfig::default()
};
configure_logging(cfg);
enable_json_capture_for_tests();
log::debug!(target: "tests.mod_override", "debug.should.pass");
log::debug!(target: "tests.other_module", "debug.must.be.filtered");
let raw = take_json_capture();
reset_logging_config_for_tests();
Logger::drain_events_for_tests();
let output = String::from_utf8(raw).expect("utf8");
assert!(
output.contains("debug.should.pass"),
"DEBUG must pass for module with DEBUG override; got: {output}"
);
assert!(
!output.contains("debug.must.be.filtered"),
"DEBUG must be filtered for module without override; got: {output}"
);
}
#[test]
fn logger_test_console_format_includes_timestamp_when_enabled() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
let cfg = LoggingConfig {
fmt: "console".to_string(),
include_timestamp: true,
..LoggingConfig::default()
};
configure_logging(cfg);
enable_console_capture_for_tests();
let logger = get_logger(Some("tests.console_ts"));
logger.info("console.timestamp.enabled");
let raw = take_console_capture();
reset_logging_config_for_tests();
Logger::drain_events_for_tests();
let line = String::from_utf8(raw).expect("utf8");
let line = line.trim();
assert!(
line.starts_with("20"),
"timestamp must appear when enabled: {line}"
);
assert!(
line.contains('T'),
"timestamp must have T separator: {line}"
);
}
#[test]
fn logger_test_console_format_includes_context_fields() {
let _guard = logger_lock().lock().expect("logger lock poisoned");
let cfg = LoggingConfig {
fmt: "console".to_string(),
include_timestamp: false,
..LoggingConfig::default()
};
configure_logging(cfg);
enable_console_capture_for_tests();
let _ctx = bind_context([(
"request_id".to_string(),
serde_json::Value::String("ctx-abc".into()),
)]);
let logger = get_logger(Some("tests.console_ctx"));
logger.info("console.context.fields");
drop(_ctx);
let raw = take_console_capture();
reset_logging_config_for_tests();
Logger::drain_events_for_tests();
let line = String::from_utf8(raw).expect("utf8");
let line = line.trim();
assert!(
line.contains("request_id"),
"context key must appear in console line: {line}"
);
assert!(
line.contains("ctx-abc"),
"context value must appear in console line: {line}"
);
}