#![cfg(feature = "setup")]
use parking_lot::Mutex;
#[allow(clippy::disallowed_types)]
use std::sync::Arc;
use rskit_logging::context::{
component_span, request_span, set_correlation_id, set_trace_id, set_user_id,
};
use rskit_logging::{
LogFormat, LogOutput, LoggingConfig, Masker, MaskingConfig, MaskingMakeWriter, SamplingConfig,
init_logging, init_logging_with_masking, init_logging_with_options,
};
use std::io::Write;
use std::sync::Arc as StdArc;
use tracing::subscriber::with_default;
use tracing::{Subscriber, info_span};
use tracing_subscriber::fmt::MakeWriter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::{EnvFilter, Layer, Registry};
#[derive(Clone, Default)]
struct CapturedLogs {
events: Arc<Mutex<Vec<CapturedEvent>>>,
spans: Arc<Mutex<Vec<CapturedSpan>>>,
}
#[derive(Debug, Clone)]
struct CapturedEvent {
message: String,
level: tracing::Level,
target: String,
fields: Vec<(String, String)>,
}
#[derive(Debug, Clone)]
struct CapturedSpan {
name: String,
fields: Vec<(String, String)>,
}
struct CapturingLayer {
logs: CapturedLogs,
}
impl<S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>> Layer<S>
for CapturingLayer
{
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let mut visitor = FieldVisitor::default();
event.record(&mut visitor);
let message = visitor
.fields
.iter()
.find(|(k, _)| k == "message")
.map(|(_, v)| v.clone())
.unwrap_or_default();
self.logs.events.lock().push(CapturedEvent {
message,
level: *event.metadata().level(),
target: event.metadata().target().to_string(),
fields: visitor.fields,
});
}
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
_id: &tracing::span::Id,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let mut visitor = FieldVisitor::default();
attrs.record(&mut visitor);
self.logs.spans.lock().push(CapturedSpan {
name: attrs.metadata().name().to_string(),
fields: visitor.fields,
});
}
}
#[derive(Default)]
struct FieldVisitor {
fields: Vec<(String, String)>,
}
impl tracing::field::Visit for FieldVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.fields
.push((field.name().to_string(), format!("{:?}", value)));
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.fields
.push((field.name().to_string(), value.to_string()));
}
}
fn capture_subscriber(filter: &str) -> (impl Subscriber + Send + Sync, CapturedLogs) {
let captured = CapturedLogs::default();
let layer = CapturingLayer {
logs: captured.clone(),
};
let subscriber = Registry::default().with(EnvFilter::new(filter)).with(layer);
(subscriber, captured)
}
#[test]
fn default_config_has_expected_values() {
let cfg = LoggingConfig::default();
assert_eq!(cfg.level, "info");
assert_eq!(cfg.format, LogFormat::Console);
assert_eq!(cfg.output, LogOutput::Stdout);
assert!(cfg.service_name.is_none());
assert!(!cfg.with_caller);
}
#[test]
fn init_logging_console_does_not_panic() {
let cfg = LoggingConfig::default();
let _guard = init_logging(&cfg).unwrap();
tracing::info!("console smoke test");
}
#[test]
fn init_logging_json_does_not_panic() {
let cfg = LoggingConfig {
format: LogFormat::Json,
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!("json smoke test");
}
#[test]
fn init_logging_with_custom_level() {
let cfg = LoggingConfig {
level: "debug".to_string(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::debug!("debug level test");
}
#[test]
fn init_logging_with_trace_level() {
let cfg = LoggingConfig {
level: "trace".to_string(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::trace!("trace level test");
}
#[test]
fn init_logging_with_warn_level() {
let cfg = LoggingConfig {
level: "warn".to_string(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::warn!("warn level test");
}
#[test]
fn init_logging_with_error_level() {
let cfg = LoggingConfig {
level: "error".to_string(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::error!("error level test");
}
#[test]
fn init_logging_with_service_name() {
let cfg = LoggingConfig {
service_name: Some("my-service".to_string()),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!(service = "my-service", "service name test");
}
#[test]
fn init_logging_stderr_output() {
let cfg = LoggingConfig {
output: LogOutput::Stderr,
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!("stderr output test");
}
#[test]
fn init_logging_file_output_writes_to_configured_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("app.log");
let cfg = LoggingConfig {
output: LogOutput::File {
path: path.to_string_lossy().into_owned(),
},
..Default::default()
};
{
let _guard = init_logging(&cfg).unwrap();
tracing::info!("file output test");
}
let output = std::fs::read_to_string(path).unwrap();
assert!(output.contains("file output test"), "output: {output}");
}
#[test]
fn init_logging_reports_file_open_errors_without_installing_successful_guard() {
let dir = tempfile::tempdir().unwrap();
let cfg = LoggingConfig {
output: LogOutput::File {
path: dir
.path()
.join("missing-parent")
.join("app.log")
.to_string_lossy()
.into_owned(),
},
..Default::default()
};
let error = match init_logging(&cfg) {
Ok(_) => panic!("missing parent directory should fail"),
Err(error) => error,
};
assert_eq!(error.code(), rskit_errors::ErrorCode::NotFound);
assert!(error.message().contains("open log output file"));
}
#[test]
fn init_logging_with_options_combines_sampling_module_levels_and_masking() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("masked.log");
let cfg = LoggingConfig {
format: LogFormat::Json,
output: LogOutput::File {
path: path.to_string_lossy().into_owned(),
},
..Default::default()
};
let sampling = SamplingConfig {
enabled: true,
initial_rate: 10,
thereafter_rate: 2,
};
let module_levels =
std::collections::HashMap::from([("rskit_logging_test".to_owned(), "trace".to_owned())]);
let masking = MaskingConfig {
enabled: true,
field_names: vec!["tenant_secret".to_owned()],
value_patterns: vec![r"tenant-[0-9]+".to_owned()],
replacement: "[MASKED]".to_owned(),
};
{
let _guard =
init_logging_with_options(&cfg, Some(&sampling), Some(&module_levels), Some(&masking))
.unwrap();
tracing::info!(
target: "rskit_logging_test",
tenant_secret = "tenant-123",
email = "user@example.com",
"masking options"
);
}
let output = std::fs::read_to_string(path).unwrap();
assert!(output.contains("[MASKED]"), "output: {output}");
assert!(output.contains("***@***.***"), "output: {output}");
assert!(!output.contains("tenant-123"), "output: {output}");
assert!(!output.contains("user@example.com"), "output: {output}");
}
#[test]
fn init_logging_with_masking_disabled_writes_original_values() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("unmasked.log");
let cfg = LoggingConfig {
output: LogOutput::File {
path: path.to_string_lossy().into_owned(),
},
..Default::default()
};
let masking = MaskingConfig {
enabled: false,
..Default::default()
};
{
let _guard = init_logging_with_masking(&cfg, &masking).unwrap();
tracing::info!(email = "user@example.com", "unmasked options");
}
let output = std::fs::read_to_string(path).unwrap();
assert!(output.contains("user@example.com"), "output: {output}");
}
#[test]
fn init_logging_with_options_console_masking_redacts_configured_fields() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("console-masked.log");
let cfg = LoggingConfig {
output: LogOutput::File {
path: path.to_string_lossy().into_owned(),
},
..Default::default()
};
let masking = MaskingConfig {
field_names: vec!["session_secret".to_owned()],
value_patterns: vec![r"abc[0-9]+".to_owned()],
replacement: "[HIDDEN]".to_owned(),
..Default::default()
};
{
let _guard = init_logging_with_options(&cfg, None, None, Some(&masking)).unwrap();
tracing::info!(session_secret = "abc123", user = "alice", "console masking");
}
let output = std::fs::read_to_string(path).unwrap();
assert!(output.contains("[HIDDEN]"), "output: {output}");
assert!(output.contains("alice"), "output: {output}");
assert!(!output.contains("abc123"), "output: {output}");
}
#[test]
fn init_logging_with_options_json_without_masking_preserves_output() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("json-unmasked.log");
let cfg = LoggingConfig {
format: LogFormat::Json,
output: LogOutput::File {
path: path.to_string_lossy().into_owned(),
},
..Default::default()
};
let sampling = SamplingConfig {
enabled: true,
initial_rate: 5,
thereafter_rate: 1,
};
{
let _guard = init_logging_with_options(&cfg, Some(&sampling), None, None).unwrap();
tracing::info!(email = "user@example.com", "json unmasked options");
}
let output = std::fs::read_to_string(path).unwrap();
assert!(
output.contains("\"email\":\"user@example.com\""),
"output: {output}"
);
assert!(
output.contains("\"message\":\"json unmasked options\""),
"output: {output}"
);
}
#[test]
fn masking_make_writer_masks_buffer_on_explicit_flush() {
let output = Arc::new(Mutex::new(Vec::<u8>::new()));
let writer = SharedBufferWriter {
output: Arc::clone(&output),
};
let masker: StdArc<dyn Masker> = StdArc::new(rskit_logging::DefaultMasker::default());
let make_writer = MaskingMakeWriter::new(writer, masker);
{
let mut writer = make_writer.make_writer();
writer
.write_all(br#"{"password":"hunter2","name":"Alice"}"#)
.unwrap();
writer.flush().unwrap();
}
let bytes = output.lock().clone();
let rendered = String::from_utf8(bytes).unwrap();
assert!(
rendered.contains(r#""password":"[REDACTED]""#),
"output: {rendered}"
);
assert!(rendered.contains(r#""name":"Alice""#), "output: {rendered}");
assert!(!rendered.contains("hunter2"), "output: {rendered}");
}
#[test]
fn init_logging_with_options_rejects_invalid_custom_masking_pattern() {
let cfg = LoggingConfig::default();
let masking = MaskingConfig {
value_patterns: vec!["(".to_owned()],
..Default::default()
};
let error = match init_logging_with_options(&cfg, None, None, Some(&masking)) {
Ok(_) => panic!("invalid masking regex should fail"),
Err(error) => error,
};
assert_eq!(error.code(), rskit_errors::ErrorCode::InvalidFormat);
}
#[derive(Clone)]
struct SharedBufferWriter {
output: Arc<Mutex<Vec<u8>>>,
}
impl<'a> MakeWriter<'a> for SharedBufferWriter {
type Writer = SharedBuffer;
fn make_writer(&'a self) -> Self::Writer {
SharedBuffer {
output: Arc::clone(&self.output),
}
}
}
struct SharedBuffer {
output: Arc<Mutex<Vec<u8>>>,
}
impl Write for SharedBuffer {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.output.lock().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(feature = "otlp")]
#[test]
fn full_logging_setup_accepts_disabled_otlp_with_all_optional_layers() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("full.log");
let cfg = LoggingConfig {
output: LogOutput::File {
path: path.to_string_lossy().into_owned(),
},
..Default::default()
};
let sampling = SamplingConfig {
enabled: true,
initial_rate: 2,
thereafter_rate: 1,
};
let module_levels =
std::collections::HashMap::from([("rskit_logging_full".to_owned(), "debug".to_owned())]);
let masking = MaskingConfig::default();
let otlp = rskit_logging::otlp::OtlpConfig::default();
let setup = rskit_logging::LoggingSetup::new(&cfg, "svc", "test", "0.1.0")
.with_sampling(&sampling)
.with_module_levels(&module_levels)
.with_masking(&masking)
.with_otlp(&otlp);
{
let _guard = rskit_logging::init_logging_full(setup).unwrap();
tracing::debug!(target: "rskit_logging_full", "full setup user@example.com");
}
let output = std::fs::read_to_string(path).unwrap();
assert!(output.contains("***@***.***"), "output: {output}");
assert!(!output.contains("user@example.com"), "output: {output}");
}
#[cfg(feature = "otlp")]
#[test]
fn enabled_http_otlp_provider_builds_layer_and_shutdowns_without_records() {
let cfg = rskit_logging::otlp::OtlpConfig {
enabled: true,
endpoint: "http://localhost:4318/v1/logs".to_owned(),
protocol: "http".to_owned(),
headers: std::collections::HashMap::from([("x-test".to_owned(), "value".to_owned())]),
};
let provider = rskit_logging::otlp::OtlpProvider::new(&cfg, "svc", "test", "0.1.0")
.unwrap()
.expect("enabled config should create a provider");
let _layer = provider.layer::<Registry>();
provider.shutdown().unwrap();
}
#[test]
fn init_logging_env_does_not_panic() {
let _guard = rskit_logging::init_logging_env();
tracing::info!("env-only init test");
}
#[test]
fn guard_drop_restores_previous_subscriber() {
let cfg = LoggingConfig::default();
{
let _guard = init_logging(&cfg).unwrap();
tracing::info!("inside guard scope");
}
tracing::info!("after guard dropped");
}
#[test]
fn filter_info_passes_info_and_above() {
let (sub, captured) = capture_subscriber("info");
with_default(sub, || {
tracing::trace!("trace msg");
tracing::debug!("debug msg");
tracing::info!("info msg");
tracing::warn!("warn msg");
tracing::error!("error msg");
});
let events = captured.events.lock();
assert_eq!(events.len(), 3, "info filter should pass info, warn, error");
assert_eq!(events[0].level, tracing::Level::INFO);
assert_eq!(events[1].level, tracing::Level::WARN);
assert_eq!(events[2].level, tracing::Level::ERROR);
}
#[test]
fn filter_debug_passes_debug_and_above() {
let (sub, captured) = capture_subscriber("debug");
with_default(sub, || {
tracing::trace!("trace msg");
tracing::debug!("debug msg");
tracing::info!("info msg");
});
let events = captured.events.lock();
assert_eq!(events.len(), 2, "debug filter should pass debug, info");
assert_eq!(events[0].level, tracing::Level::DEBUG);
assert_eq!(events[1].level, tracing::Level::INFO);
}
#[test]
fn filter_error_only() {
let (sub, captured) = capture_subscriber("error");
with_default(sub, || {
tracing::info!("info msg");
tracing::warn!("warn msg");
tracing::error!("error msg");
});
let events = captured.events.lock();
assert_eq!(events.len(), 1);
assert_eq!(events[0].level, tracing::Level::ERROR);
}
#[test]
fn filter_trace_passes_everything() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
tracing::trace!("trace msg");
tracing::debug!("debug msg");
tracing::info!("info msg");
tracing::warn!("warn msg");
tracing::error!("error msg");
});
let events = captured.events.lock();
assert_eq!(events.len(), 5, "trace filter should pass all levels");
}
#[test]
fn filter_with_target_directive() {
let (sub, captured) = capture_subscriber("warn,integration=trace");
with_default(sub, || {
tracing::trace!(target: "integration", "targeted trace");
tracing::debug!(target: "other_crate", "other debug");
tracing::warn!(target: "other_crate", "other warn");
});
let events = captured.events.lock();
assert_eq!(events.len(), 2);
assert_eq!(events[0].target, "integration");
assert_eq!(events[1].target, "other_crate");
}
#[test]
fn envfilter_parse_valid_strings() {
for level_str in &["trace", "debug", "info", "warn", "error", "off"] {
let filter = EnvFilter::try_new(level_str);
assert!(filter.is_ok(), "Failed to parse filter: {}", level_str);
}
}
#[test]
fn envfilter_parse_compound_directive() {
let filter = EnvFilter::try_new("info,rskit=trace,hyper=warn");
assert!(filter.is_ok());
}
#[test]
fn envfilter_parse_invalid_string_handled_by_init_logging() {
let cfg = LoggingConfig {
level: "not_a_real_module=trace".to_string(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!("fallback test");
}
#[test]
fn set_correlation_id_does_not_panic_outside_span() {
let (sub, _captured) = capture_subscriber("trace");
with_default(sub, || {
set_correlation_id("abc-123");
});
}
#[test]
fn set_user_id_does_not_panic_outside_span() {
let (sub, _captured) = capture_subscriber("trace");
with_default(sub, || {
set_user_id("user-42");
});
}
#[test]
fn set_trace_id_does_not_panic_outside_span() {
let (sub, _captured) = capture_subscriber("trace");
with_default(sub, || {
set_trace_id("trace-xyz");
});
}
#[test]
fn correlation_id_recorded_inside_span_with_field() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
let span = info_span!("test_op", correlation_id = tracing::field::Empty);
let _enter = span.enter();
set_correlation_id("corr-456");
tracing::info!("after setting correlation_id");
});
let events = captured.events.lock();
assert_eq!(events.len(), 1);
assert_eq!(events[0].message, "after setting correlation_id");
}
#[test]
fn user_id_recorded_inside_span_with_field() {
let (sub, _captured) = capture_subscriber("trace");
with_default(sub, || {
let span = info_span!("test_op", user_id = tracing::field::Empty);
let _enter = span.enter();
set_user_id("user-99");
tracing::info!("after setting user_id");
});
}
#[test]
fn trace_id_recorded_inside_span_with_field() {
let (sub, _captured) = capture_subscriber("trace");
with_default(sub, || {
let span = info_span!("test_op", trace_id = tracing::field::Empty);
let _enter = span.enter();
set_trace_id("tid-abc");
tracing::info!("after setting trace_id");
});
}
#[test]
fn component_span_creates_named_span() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
let _s = component_span("auth-service").entered();
tracing::info!("inside component span");
});
let spans = captured.spans.lock();
assert!(!spans.is_empty());
assert_eq!(spans[0].name, "component");
let comp_field = spans[0].fields.iter().find(|(k, _)| k == "component");
assert!(comp_field.is_some(), "component field should be present");
assert_eq!(comp_field.unwrap().1, "auth-service");
}
#[test]
fn request_span_captures_http_metadata() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
let _s = request_span("GET", "/api/v1/health", "req-001").entered();
tracing::info!("inside request span");
});
let spans = captured.spans.lock();
assert!(!spans.is_empty());
assert_eq!(spans[0].name, "request");
let field_map: std::collections::HashMap<_, _> = spans[0].fields.iter().cloned().collect();
assert_eq!(
field_map.get("http.method").map(|s| s.as_str()),
Some("GET")
);
assert_eq!(
field_map.get("http.path").map(|s| s.as_str()),
Some("/api/v1/health")
);
assert_eq!(
field_map.get("request_id").map(|s| s.as_str()),
Some("req-001")
);
}
#[test]
fn request_span_various_methods() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
for method in &["GET", "POST", "PUT", "DELETE", "PATCH"] {
let _s = request_span(method, "/test", "rid").entered();
}
});
let spans = captured.spans.lock();
assert_eq!(spans.len(), 5);
}
#[test]
fn nested_component_and_request_spans() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
let _comp = component_span("gateway").entered();
let _req = request_span("POST", "/login", "req-login").entered();
tracing::info!("nested span test");
});
let spans = captured.spans.lock();
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].name, "component");
assert_eq!(spans[1].name, "request");
}
#[test]
fn json_format_config_round_trip() {
let cfg = LoggingConfig {
format: LogFormat::Json,
..Default::default()
};
assert_eq!(cfg.format, LogFormat::Json);
let _guard = init_logging(&cfg).unwrap();
tracing::info!(key = "value", "json format round trip");
}
#[test]
fn console_format_config_round_trip() {
let cfg = LoggingConfig {
format: LogFormat::Console,
..Default::default()
};
assert_eq!(cfg.format, LogFormat::Console);
let _guard = init_logging(&cfg).unwrap();
tracing::info!("console format round trip");
}
#[test]
fn switching_formats_between_guards() {
{
let cfg = LoggingConfig {
format: LogFormat::Json,
..Default::default()
};
let _g = init_logging(&cfg).unwrap();
tracing::info!("json");
}
{
let cfg = LoggingConfig {
format: LogFormat::Console,
..Default::default()
};
let _g = init_logging(&cfg).unwrap();
tracing::info!("console");
}
}
#[test]
fn empty_level_string_uses_envfilter_default() {
let cfg = LoggingConfig {
level: String::new(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!("empty level test");
}
#[test]
fn empty_service_name() {
let cfg = LoggingConfig {
service_name: Some(String::new()),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!(service = "", "empty service name test");
}
#[test]
fn very_long_message() {
let long_msg = "x".repeat(100_000);
let (sub, captured) = capture_subscriber("info");
with_default(sub, || {
tracing::info!("{}", long_msg);
});
let events = captured.events.lock();
assert_eq!(events.len(), 1);
assert!(events[0].message.len() >= 100_000);
}
#[test]
fn unicode_in_messages() {
let (sub, captured) = capture_subscriber("info");
with_default(sub, || {
tracing::info!("こんにちは世界 🌍 مرحبا");
});
let events = captured.events.lock();
assert_eq!(events.len(), 1);
assert!(events[0].message.contains("こんにちは世界"));
assert!(events[0].message.contains("🌍"));
assert!(events[0].message.contains("مرحبا"));
}
#[test]
fn unicode_in_span_fields() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
let _s = request_span("GET", "/路径/テスト", "req-ünïcödé").entered();
tracing::info!("unicode span test");
});
let spans = captured.spans.lock();
let field_map: std::collections::HashMap<_, _> = spans[0].fields.iter().cloned().collect();
assert_eq!(
field_map.get("http.path").map(|s| s.as_str()),
Some("/路径/テスト")
);
assert_eq!(
field_map.get("request_id").map(|s| s.as_str()),
Some("req-ünïcödé")
);
}
#[test]
fn multiple_init_calls_with_different_configs() {
for format in &[LogFormat::Console, LogFormat::Json, LogFormat::Console] {
let cfg = LoggingConfig {
format: format.clone(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!("reinit with {:?}", format);
}
}
#[test]
fn concurrent_logging_does_not_panic() {
let cfg = LoggingConfig {
level: "info".to_string(),
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
let handles: Vec<_> = (0..8)
.map(|i| {
std::thread::spawn(move || {
for j in 0..100 {
tracing::info!(thread = i, iteration = j, "concurrent log");
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn special_characters_in_field_values() {
let (sub, captured) = capture_subscriber("info");
with_default(sub, || {
tracing::info!(
path = r#"C:\Users\test\file"with"quotes"#,
query = "SELECT * FROM t WHERE x='1' AND y=\"2\"",
"special chars"
);
});
let events = captured.events.lock();
assert_eq!(events.len(), 1);
let field_map: std::collections::HashMap<_, _> = events[0].fields.iter().cloned().collect();
assert_eq!(
field_map.get("path").map(|s| s.as_str()),
Some(r#"C:\Users\test\file"with"quotes"#)
);
assert_eq!(
field_map.get("query").map(|s| s.as_str()),
Some("SELECT * FROM t WHERE x='1' AND y=\"2\"")
);
}
#[test]
fn with_caller_config_field_accepted() {
let cfg = LoggingConfig {
with_caller: true,
..Default::default()
};
assert!(cfg.with_caller);
let _guard = init_logging(&cfg).unwrap();
tracing::info!("caller location test");
}
#[test]
fn reexported_macros_compile_and_run() {
let (sub, captured) = capture_subscriber("trace");
with_default(sub, || {
rskit_logging::trace!("re-export trace");
rskit_logging::debug!("re-export debug");
rskit_logging::info!("re-export info");
rskit_logging::warn!("re-export warn");
rskit_logging::error!("re-export error");
});
let events = captured.events.lock();
assert_eq!(events.len(), 5);
}
#[test]
fn logging_config_deserializes_from_json() {
let json = r#"{"level":"debug","format":"json"}"#;
let cfg: LoggingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.level, "debug");
assert_eq!(cfg.format, LogFormat::Json);
}
#[test]
fn logging_config_deserializes_console_format() {
let json = r#"{"format":"console"}"#;
let cfg: LoggingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.format, LogFormat::Console);
}
#[test]
fn logging_config_defaults_when_fields_missing() {
let json = r#"{}"#;
let cfg: LoggingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.level, "info");
assert_eq!(cfg.format, LogFormat::Console);
assert_eq!(cfg.output, LogOutput::Stdout);
assert!(cfg.service_name.is_none());
}
#[test]
fn logging_config_stderr_output_deserialization() {
let json = r#"{"output":{"type":"stderr"}}"#;
let cfg: LoggingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.output, LogOutput::Stderr);
}
#[test]
fn logging_config_file_output_deserialization() {
let json = r#"{"output":{"type":"file","path":"/var/log/app.log"}}"#;
let cfg: LoggingConfig = serde_json::from_str(json).unwrap();
assert!(matches!(cfg.output, LogOutput::File { ref path } if path == "/var/log/app.log"));
}
#[test]
fn logging_config_with_service_name() {
let json = r#"{"service_name":"order-service"}"#;
let cfg: LoggingConfig = serde_json::from_str(json).unwrap();
assert_eq!(cfg.service_name.as_deref(), Some("order-service"));
}
#[test]
fn logging_config_with_caller_true() {
let json = r#"{"with_caller":true}"#;
let cfg: LoggingConfig = serde_json::from_str(json).unwrap();
assert!(cfg.with_caller);
}