use foundations::telemetry::TelemetryContext;
use foundations::telemetry::TestTelemetryContext;
use foundations::telemetry::log::internal::LoggerWithKvNestingTracking;
use foundations::telemetry::log::{add_fields, freeze, is_frozen, set_verbosity, unfreeze, warn};
use foundations::telemetry::settings::{LogVerbosity, LoggingSettings, RateLimitingSettings};
use foundations_macros::with_test_telemetry;
#[with_test_telemetry(test)]
fn test_rate_limiter(mut ctx: TestTelemetryContext) {
for i in 0..16 {
warn!("{}", i);
}
assert_eq!(ctx.log_records().len(), 16);
ctx.set_logging_settings(LoggingSettings {
rate_limit: RateLimitingSettings {
enabled: true,
max_events_per_second: 5,
},
..Default::default()
});
for i in 16..32 {
warn!("{}", i);
}
assert!(ctx.log_records().len() < 32);
}
#[with_test_telemetry(test)]
fn test_exceed_limit_kv_nesting(_ctx: TestTelemetryContext) {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
for _ in 0..((LoggerWithKvNestingTracking::MAX_NESTING / 2) + 1) {
add_fields! { "key1" => "hello" }
set_verbosity(LogVerbosity::Info).expect("set_verbosity");
}
})) {
Ok(_) => panic!("test case exceeded the maximum log KV nesting, but there was no panic"),
Err(err) => {
if let Some(msg) = err.downcast_ref::<&'static str>() {
assert_eq!(
*msg,
LoggerWithKvNestingTracking::EXCEEDED_MAX_NESTING_ERROR
);
} else if let Some(msg) = err.downcast_ref::<String>() {
assert_eq!(
*msg,
LoggerWithKvNestingTracking::EXCEEDED_MAX_NESTING_ERROR
);
} else {
panic!(
"test case exceeded the maximum log KV nesting, but the panic was not castable to the expected type"
);
}
}
}
}
#[with_test_telemetry(test)]
fn test_not_exceed_limit_kv_nesting(_ctx: TestTelemetryContext) {
for _ in 0..((LoggerWithKvNestingTracking::MAX_NESTING / 2) - 5) {
add_fields! { "key1" => "hello" }
set_verbosity(LogVerbosity::Info).expect("set_verbosity");
}
}
#[with_test_telemetry(test)]
fn test_is_frozen(_ctx: TestTelemetryContext) {
assert!(!is_frozen(), "logger should start unfrozen");
add_fields! { "request_id" => "abc123" }
set_verbosity(LogVerbosity::Debug).expect("set_verbosity on logger creation");
freeze();
assert!(is_frozen());
unfreeze();
add_fields! { "response_id" => "abc123" }
set_verbosity(LogVerbosity::Info).expect("set_verbosity after unfreeze");
assert!(!is_frozen());
}
#[with_test_telemetry(test)]
fn test_fork_resets_frozen(_ctx: TestTelemetryContext) {
freeze();
assert!(is_frozen());
let forked_ctx = TelemetryContext::current().with_forked_log();
let _scope = forked_ctx.scope();
assert!(
!is_frozen(),
"forked logger should start unfrozen regardless of parent's frozen state"
);
add_fields! { "request_id" => "abc123" }
set_verbosity(LogVerbosity::Info).expect("set_verbosity on forked logger");
}
#[with_test_telemetry(test)]
fn test_frozen_logger_add_fields_panics(_ctx: TestTelemetryContext) {
freeze();
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
add_fields! { "key" => "value" }
})) {
Ok(_) => panic!("expected a panic when calling add_fields! on a frozen logger"),
Err(err) => {
if let Some(msg) = err.downcast_ref::<&'static str>() {
assert_eq!(*msg, LoggerWithKvNestingTracking::FROZEN_LOGGER_ERROR);
} else if let Some(msg) = err.downcast_ref::<String>() {
assert_eq!(*msg, LoggerWithKvNestingTracking::FROZEN_LOGGER_ERROR);
} else {
panic!("panic payload was not castable to the expected type");
}
}
}
}
#[cfg(feature = "tracing-rs-compat")]
mod tracing_rs_compat {
use std::io;
use std::sync::{Arc, Mutex};
use foundations::telemetry::TelemetryContext;
use foundations::telemetry::log::{TestLogRecord, warn};
use foundations::telemetry::settings::LoggingSettings;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::util::SubscriberInitExt as _;
struct TestWriter {
log_entries: Arc<Mutex<Vec<String>>>,
}
impl TestWriter {
fn with_entries(entries: Arc<Mutex<Vec<String>>>) -> Self {
Self {
log_entries: entries,
}
}
}
impl io::Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let s = String::from_utf8(buf.to_vec()).unwrap();
self.log_entries.lock().unwrap().push(s.trim().to_string());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
unimplemented!()
}
}
#[test]
fn test_tracing_rs_compat() {
let entries = Arc::new(Mutex::new(Vec::new()));
let tracing_log_entries = entries.clone();
let _subscriber = tracing_subscriber::fmt()
.with_max_level(LevelFilter::TRACE)
.with_writer(move || TestWriter::with_entries(entries.clone()))
.without_time()
.with_level(true)
.with_ansi(false)
.set_default();
let settings = LoggingSettings {
output: foundations::telemetry::settings::LogOutput::TracingRsCompat,
..Default::default()
};
let mut ctx = TelemetryContext::test();
ctx.set_tracing_rs_log_drain(settings);
let _scope = ctx.scope();
warn!("compat-layer-works");
let slog_records = ctx.log_records();
let expected_slog_records = TestLogRecord {
level: slog::Level::Warning,
message: "compat-layer-works".to_string(),
fields: vec![],
};
assert_eq!(*slog_records, vec![expected_slog_records]);
let tracing_record = tracing_log_entries
.lock()
.unwrap()
.first()
.cloned()
.unwrap();
assert!(tracing_record.contains("WARN slog: compat-layer-works"));
}
}