use crate::LogRecord;
use crate::Metrics;
use crate::support::processing::RateLimiter;
use crate::validation::sanitize::LogSanitizer;
use crossbeam_channel::Sender;
use parking_lot::Mutex;
use serde_json::value;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use tracing::{Event, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
const DEFAULT_SEND_TIMEOUT_MS: u64 = 100;
const FALLBACK_BUFFER_SIZE: usize = 100;
const ERROR_SAMPLING_RATE: u64 = 100;
struct FallbackEntry {
record: Arc<LogRecord>,
delivered: Vec<bool>,
}
impl Clone for LoggerSubscriber {
fn clone(&self) -> Self {
Self {
console_sender: self.console_sender.clone(),
async_sender: self.async_sender.clone(),
extra_async_senders: self.extra_async_senders.clone(),
metrics: self.metrics.clone(),
send_timeout_ms: self.send_timeout_ms,
fallback_buffer: self.fallback_buffer.clone(),
sanitizer: self.sanitizer.clone(),
rate_limiter: self.rate_limiter.clone(),
error_sample_counter: AtomicU64::new(self.error_sample_counter.load(Ordering::Relaxed)),
}
}
}
pub struct LoggerSubscriber {
console_sender: Sender<Arc<LogRecord>>,
async_sender: Sender<Arc<LogRecord>>,
extra_async_senders: Vec<Sender<Arc<LogRecord>>>,
metrics: Arc<Metrics>,
send_timeout_ms: u64,
fallback_buffer: Arc<Mutex<VecDeque<FallbackEntry>>>,
sanitizer: Option<Arc<LogSanitizer>>,
rate_limiter: Option<Arc<RateLimiter>>,
error_sample_counter: AtomicU64,
}
impl LoggerSubscriber {
pub fn new(
console_sender: Sender<Arc<LogRecord>>,
async_sender: Sender<Arc<LogRecord>>,
metrics: Arc<Metrics>,
) -> Self {
Self {
console_sender,
async_sender,
extra_async_senders: Vec::new(),
metrics,
send_timeout_ms: DEFAULT_SEND_TIMEOUT_MS,
fallback_buffer: Arc::new(Mutex::new(VecDeque::with_capacity(FALLBACK_BUFFER_SIZE))),
sanitizer: None,
rate_limiter: None,
error_sample_counter: AtomicU64::new(0),
}
}
pub fn with_extra_async_sender(mut self, sender: Sender<Arc<LogRecord>>) -> Self {
self.extra_async_senders.push(sender);
self
}
fn send_to_async_sinks(&self, record: &Arc<LogRecord>, timeout: Duration) -> Vec<bool> {
let mut delivered = Vec::with_capacity(1 + self.extra_async_senders.len());
delivered.push(
self.async_sender
.send_timeout(Arc::clone(record), timeout)
.is_ok(),
);
for sender in &self.extra_async_senders {
delivered.push(sender.send_timeout(Arc::clone(record), timeout).is_ok());
}
delivered
}
pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
self.send_timeout_ms = timeout_ms;
self
}
pub fn with_sanitizer(mut self, sanitizer: Arc<LogSanitizer>) -> Self {
self.sanitizer = Some(sanitizer);
self
}
pub fn with_rate_limiter(mut self, rate_limiter: Arc<RateLimiter>) -> Self {
self.rate_limiter = Some(rate_limiter);
self
}
fn is_critical_level(level: &str) -> bool {
level == "ERROR" || level == "FATAL"
}
fn extract_trace_context<S>(ctx: &Context<'_, S>, record: &mut LogRecord)
where
S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
let current = ctx.current_span();
let Some(id) = current.id() else {
return;
};
record.span_id = Some(format!("{:016x}", id.into_u64()));
if let Some(value::Value::String(explicit)) = record.fields.get("trace_id") {
record.trace_id = Some(explicit.clone());
}
if record.trace_id.is_none() {
record.trace_id = ctx.span(id).and_then(|span| {
span.scope()
.last()
.map(|root| format!("{:032x}", root.id().into_u64()))
});
}
if let Some(value::Value::String(explicit)) = record.fields.get("span_id") {
record.span_id = Some(explicit.clone());
}
}
fn sanitize_record(&self, record: &mut LogRecord) {
if let Some(ref sanitizer) = self.sanitizer {
record.message = sanitizer.sanitize(&record.message);
for value in record.fields.values_mut() {
Self::sanitize_field_value(sanitizer, value);
}
}
}
fn sanitize_field_value(sanitizer: &LogSanitizer, value: &mut value::Value) {
match value {
value::Value::String(s) => *s = sanitizer.sanitize(s),
value::Value::Array(items) => {
for item in items.iter_mut() {
Self::sanitize_field_value(sanitizer, item);
}
}
value::Value::Object(map) => {
for (nested_key, nested_value) in map.iter_mut() {
if LogRecord::is_sensitive_key(nested_key) {
if let value::Value::String(s) = nested_value {
*s = sanitizer.sanitize(s);
} else {
Self::sanitize_field_value(sanitizer, nested_value);
}
} else {
Self::sanitize_field_value(sanitizer, nested_value);
}
}
}
_ => {}
}
}
pub fn try_flush_fallback(&self) {
let batch: Vec<FallbackEntry> = {
let mut buffer = self.fallback_buffer.lock();
buffer.drain(..).collect()
};
if batch.is_empty() {
return;
}
let timeout = Duration::from_millis(self.send_timeout_ms);
let channel_count = 1 + self.extra_async_senders.len();
let mut undelivered: VecDeque<FallbackEntry> = VecDeque::new();
let mut stopped = false;
for mut entry in batch {
if stopped {
undelivered.push_back(entry);
continue;
}
let mut complete = true;
for idx in 0..channel_count {
if entry.delivered.get(idx).copied().unwrap_or(false) {
continue;
}
let sender = if idx == 0 {
&self.async_sender
} else {
&self.extra_async_senders[idx - 1]
};
if sender
.send_timeout(Arc::clone(&entry.record), timeout)
.is_ok()
{
if idx < entry.delivered.len() {
entry.delivered[idx] = true;
}
} else {
complete = false;
break;
}
}
if !complete {
undelivered.push_back(entry);
stopped = true;
}
}
if !undelivered.is_empty() {
let mut buffer = self.fallback_buffer.lock();
for entry in undelivered.into_iter().rev() {
buffer.push_front(entry);
}
}
}
}
impl Drop for LoggerSubscriber {
fn drop(&mut self) {
let buffer_len = {
let buffer = self.fallback_buffer.lock();
buffer.len()
};
if buffer_len > 0 {
self.try_flush_fallback();
let remaining = self.fallback_buffer.lock().len();
if remaining > 0 {
let warning =
format!("LoggerSubscriber dropped with {remaining} unflushed fallback records");
eprintln!("{warning}");
}
}
}
}
impl<S> Layer<S> for LoggerSubscriber
where
S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
{
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
let mut record = LogRecord::from_event(event);
Self::extract_trace_context(&ctx, &mut record);
if let Some(ref limiter) = self.rate_limiter
&& !limiter.try_acquire()
{
if Self::is_critical_level(&record.level) {
let count = self.error_sample_counter.fetch_add(1, Ordering::Relaxed);
if !count.is_multiple_of(ERROR_SAMPLING_RATE) {
self.metrics.inc_logs_dropped();
return;
}
} else {
self.metrics.inc_logs_dropped();
return;
}
}
self.sanitize_record(&mut record);
let record = Arc::new(record);
match self.console_sender.try_send(Arc::clone(&record)) {
Ok(_) => {}
Err(crossbeam_channel::TrySendError::Full(_)) => {
self.metrics.inc_channel_blocked();
self.metrics.inc_logs_dropped();
}
Err(crossbeam_channel::TrySendError::Disconnected(_)) => {
self.metrics.inc_logs_dropped();
}
}
let timeout = Duration::from_millis(self.send_timeout_ms);
let delivered = self.send_to_async_sinks(&record, timeout);
if delivered.iter().any(|ok| !ok) {
if Self::is_critical_level(&record.level) {
let mut buffer = self.fallback_buffer.lock();
if buffer.len() >= FALLBACK_BUFFER_SIZE {
buffer.pop_front();
}
buffer.push_back(FallbackEntry { record, delivered });
} else {
self.metrics.inc_logs_dropped();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossbeam_channel::bounded;
use serde_json::Value;
use serial_test::serial;
use tracing::subscriber::with_default;
use tracing_subscriber::prelude::*;
#[test]
fn test_on_event_sends_to_channels() {
let (console_tx, console_rx) = bounded(10);
let (async_tx, async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::subscriber", message = "hello", user_id = 1u64);
});
let console_received = console_rx.recv().unwrap();
assert_eq!(console_received.level, "INFO");
assert_eq!(console_received.target, "test::subscriber");
assert_eq!(console_received.message, "hello");
let async_received = async_rx.recv().unwrap();
assert_eq!(async_received.level, "INFO");
assert_eq!(async_received.target, "test::subscriber");
assert_eq!(async_received.message, "hello");
}
#[test]
fn test_on_event_handles_full_channel() {
let (console_tx, console_rx) = bounded(1);
let (async_tx, async_rx) = bounded(1);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
for i in 0..5 {
tracing::info!(target: "test::subscriber", message = "msg {}", i);
}
});
while console_rx.try_recv().is_ok() {}
while async_rx.try_recv().is_ok() {}
}
#[test]
fn test_critical_level_adds_to_fallback_buffer() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(0);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics.clone());
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::error!(target: "test::subscriber", message = "critical error");
});
assert_eq!(metrics.logs_written(), 0);
}
#[test]
fn test_fallback_buffer_does_not_panic_on_overflow() {
let (console_tx, _cr) = bounded(10);
let (async_tx, _ar) = bounded(0);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
for i in 0..105 {
tracing::error!(target: "test::subscriber", msg = "overflow {}", i);
}
});
}
#[test]
fn test_try_flush_fallback_with_disconnected_channel() {
let (console_tx1, _cr1) = bounded(10);
let (async_tx1, _ar1) = bounded(1);
drop(_ar1);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx1.clone(), async_tx1.clone(), metrics.clone());
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::error!(target: "test::subscriber", msg = "fallback before disconnect");
});
let _subscriber_b = LoggerSubscriber::new(console_tx1, async_tx1, metrics);
_subscriber_b.try_flush_fallback();
}
#[test]
fn test_on_event_dropped_on_disconnected_async_channel() {
let (console_tx, _cr) = bounded(10);
let (async_tx, _ar) = bounded(1);
drop(_ar); let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics.clone());
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::subscriber", message = "after disconnect");
});
assert_eq!(metrics.logs_dropped(), 1);
}
#[test]
fn test_with_timeout_configures_send_timeout() {
let (console_tx, _) = bounded(10);
let (async_tx, _) = bounded(10);
let metrics = Arc::new(Metrics::new());
let subscriber = LoggerSubscriber::new(console_tx, async_tx, metrics).with_timeout(500);
assert_eq!(subscriber.send_timeout_ms, 500);
}
#[test]
fn test_try_flush_fallback_drains_buffer_on_success() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let subscriber = LoggerSubscriber::new(console_tx, async_tx, metrics);
let record = Arc::new(LogRecord::new(
tracing::Level::ERROR,
"test::fallback".to_string(),
"fallback flush test".to_string(),
));
subscriber.fallback_buffer.lock().push_back(FallbackEntry {
record,
delivered: vec![false],
});
subscriber.try_flush_fallback();
assert!(
subscriber.fallback_buffer.lock().is_empty(),
"buffer should be empty after successful flush"
);
let received = async_rx.recv_timeout(std::time::Duration::from_millis(100));
assert!(received.is_ok(), "should receive the flushed record");
assert_eq!(received.unwrap().message, "fallback flush test");
}
#[test]
fn test_try_flush_fallback_breaks_on_disconnected_channel() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let subscriber = LoggerSubscriber::new(console_tx, async_tx, metrics);
let record = Arc::new(LogRecord::new(
tracing::Level::ERROR,
"test::fallback".to_string(),
"disconnect test".to_string(),
));
subscriber.fallback_buffer.lock().push_back(FallbackEntry {
record,
delivered: vec![false],
});
drop(_async_rx);
subscriber.try_flush_fallback();
assert_eq!(
subscriber.fallback_buffer.lock().len(),
1,
"buffer should still contain the record after disconnect"
);
}
#[test]
fn test_try_flush_fallback_preserves_order_on_partial_flush() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, async_rx) = bounded(1);
let metrics = Arc::new(Metrics::new());
let subscriber = LoggerSubscriber::new(console_tx, async_tx, metrics);
for i in 0..3 {
let record = Arc::new(LogRecord::new(
tracing::Level::ERROR,
"test::fallback".to_string(),
format!("fallback-order-{i}"),
));
subscriber.fallback_buffer.lock().push_back(FallbackEntry {
record,
delivered: vec![false],
});
}
subscriber.try_flush_fallback();
let first = async_rx
.recv_timeout(std::time::Duration::from_millis(500))
.expect("first record should be flushed");
assert_eq!(first.message, "fallback-order-0");
let buffer = subscriber.fallback_buffer.lock();
assert_eq!(buffer.len(), 2, "remaining records should be refilled");
assert_eq!(
buffer.front().unwrap().record.message,
"fallback-order-1",
"refilled records must keep original order (front)"
);
assert_eq!(
buffer.back().unwrap().record.message,
"fallback-order-2",
"refilled records must keep original order (back)"
);
}
#[test]
fn test_on_event_partial_failure_records_per_channel_delivery() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, async_rx) = bounded(10);
let (extra_tx, _extra_rx) = bounded(0);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics)
.with_extra_async_sender(extra_tx)
.with_timeout(50);
let fallback_buffer = Arc::clone(&layer.fallback_buffer);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::error!(target: "test::subscriber", message = "partial delivery");
});
assert!(
async_rx.try_recv().is_ok(),
"primary async channel should receive the record"
);
assert!(
async_rx.try_recv().is_err(),
"primary async channel must not receive duplicates"
);
let buffer = fallback_buffer.lock();
assert_eq!(
buffer.len(),
1,
"record should be buffered for the failed channel"
);
let entry = buffer.front().unwrap();
assert_eq!(
entry.delivered,
vec![true, false],
"delivery state must be tracked per channel"
);
assert_eq!(entry.record.message, "partial delivery");
}
#[test]
fn test_fallback_flush_only_resends_undelivered_channels() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, async_rx) = bounded(10);
let (extra_tx, extra_rx) = bounded(0);
let metrics = Arc::new(Metrics::new());
let subscriber = LoggerSubscriber::new(console_tx, async_tx, metrics)
.with_extra_async_sender(extra_tx)
.with_timeout(500);
subscriber.fallback_buffer.lock().push_back(FallbackEntry {
record: Arc::new(LogRecord::new(
tracing::Level::ERROR,
"test::fallback".to_string(),
"dup suppression".to_string(),
)),
delivered: vec![true, false],
});
let receiver = std::thread::spawn(move || {
extra_rx.recv_timeout(std::time::Duration::from_millis(2000))
});
std::thread::sleep(std::time::Duration::from_millis(50));
subscriber.try_flush_fallback();
let resent = receiver
.join()
.unwrap()
.expect("extra channel should receive the flushed record");
assert_eq!(resent.message, "dup suppression");
assert!(
async_rx.try_recv().is_err(),
"already-delivered channel must NOT receive a duplicate on flush"
);
assert!(
subscriber.fallback_buffer.lock().is_empty(),
"fully delivered entry should be removed from the buffer"
);
}
#[test]
fn test_sanitize_record_recurses_into_nested_object_and_array() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let sanitizer = Arc::new(LogSanitizer::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics).with_sanitizer(sanitizer);
let mut record = LogRecord::new(
tracing::Level::INFO,
"test::sanitize".to_string(),
"nested sanitize".to_string(),
);
let mut nested = serde_json::Map::new();
nested.insert(
"password".to_string(),
Value::String("line1\nline2".to_string()),
);
nested.insert("note".to_string(), Value::String("a\nb".to_string()));
record
.fields
.insert("config".to_string(), Value::Object(nested));
let mut item = serde_json::Map::new();
item.insert(
"api_token".to_string(),
Value::String("tok1\ntok2".to_string()),
);
record
.fields
.insert("items".to_string(), Value::Array(vec![Value::Object(item)]));
layer.sanitize_record(&mut record);
let config = record.fields.get("config").unwrap();
if let Value::Object(map) = config {
if let Value::String(s) = map.get("password").unwrap() {
assert!(
!s.contains('\n') && s.contains("\\n"),
"nested sensitive key 'password' must be sanitized, got: {s:?}"
);
} else {
panic!("password value should remain a string");
}
if let Value::String(s) = map.get("note").unwrap() {
assert!(
!s.contains('\n'),
"nested plain string must also be sanitized, got: {s:?}"
);
}
} else {
panic!("config field should remain an object");
}
let items = record.fields.get("items").unwrap();
if let Value::Array(arr) = items {
if let Value::Object(map) = &arr[0] {
if let Value::String(s) = map.get("api_token").unwrap() {
assert!(
!s.contains('\n') && s.contains("\\n"),
"sensitive key inside array must be sanitized, got: {s:?}"
);
} else {
panic!("api_token value should remain a string");
}
} else {
panic!("array element should remain an object");
}
} else {
panic!("items field should remain an array");
}
}
#[test]
fn test_sanitize_record_leaves_non_string_values_untouched() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let sanitizer = Arc::new(LogSanitizer::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics).with_sanitizer(sanitizer);
let mut record = LogRecord::new(
tracing::Level::INFO,
"test::sanitize".to_string(),
"non-string values".to_string(),
);
record
.fields
.insert("count".to_string(), serde_json::json!(42));
layer.sanitize_record(&mut record);
assert_eq!(
record.fields.get("count").unwrap(),
&serde_json::json!(42),
"non-string values must not be modified"
);
}
#[test]
fn test_is_sensitive_key_matches_log_record_canonical_semantics() {
for key in ["password", "api_key", "auth_token", "secret"] {
assert!(
LogRecord::is_sensitive_key(key),
"'{key}' must be judged sensitive by the canonical implementation"
);
}
for key in ["primary_key", "author"] {
assert!(
!LogRecord::is_sensitive_key(key),
"'{key}' must NOT be judged sensitive by the canonical implementation"
);
}
}
#[test]
fn test_on_event_console_disconnected_increments_dropped() {
let (console_tx, _console_rx) = bounded(10);
drop(_console_rx);
let (async_tx, _async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics.clone());
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::subscriber", message = "console disconnected");
});
assert_eq!(
metrics.logs_dropped(),
1,
"console disconnect should increment logs_dropped by 1"
);
}
#[test]
fn test_on_event_console_full_channel_increments_blocked_and_dropped() {
let (console_tx, console_rx) = bounded(1);
let (async_tx, _async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics.clone());
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::subscriber", message = "first");
tracing::info!(target: "test::subscriber", message = "second");
});
while console_rx.try_recv().is_ok() {}
assert!(
metrics.logs_dropped() >= 1,
"console full should increment logs_dropped, got: {}",
metrics.logs_dropped()
);
}
#[test]
fn test_on_event_console_ok_and_async_ok_paths() {
let (console_tx, console_rx) = bounded(10);
let (async_tx, async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics.clone());
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::subscriber", message = "ok path test");
});
assert!(
console_rx.try_recv().is_ok(),
"console should receive record"
);
assert!(async_rx.try_recv().is_ok(), "async should receive record");
assert_eq!(
metrics.logs_dropped(),
0,
"Ok path should not increment logs_dropped"
);
}
#[test]
#[serial]
fn test_on_event_non_critical_async_timeout_increments_blocked_and_dropped() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(0);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics.clone());
let registry = tracing_subscriber::registry().with(layer);
let before_blocked = metrics.channel_blocked();
let before_dropped = metrics.logs_dropped();
with_default(registry, || {
tracing::info!(target: "test::subscriber", message = "non-critical timeout");
});
assert_eq!(
metrics.channel_blocked(),
before_blocked,
"non-critical async timeout should NOT increment channel_blocked"
);
assert_eq!(
metrics.logs_dropped(),
before_dropped + 1,
"non-critical async timeout should increment logs_dropped"
);
}
#[test]
#[serial]
fn test_on_event_critical_async_timeout_stores_record_in_fallback_buffer() {
let (console_tx, _console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(0);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics.clone());
let fallback_buffer = Arc::clone(&layer.fallback_buffer);
let registry = tracing_subscriber::registry().with(layer);
let before_blocked = metrics.channel_blocked();
let before_dropped = metrics.logs_dropped();
with_default(registry, || {
tracing::error!(target: "test::subscriber", message = "critical timeout");
});
let buffer_guard = fallback_buffer.lock();
assert_eq!(
buffer_guard.len(),
1,
"fallback_buffer should contain exactly 1 record"
);
let entry = buffer_guard
.front()
.expect("should have a record in fallback_buffer");
assert_eq!(entry.record.level, "ERROR", "record level should be ERROR");
assert_eq!(
entry.record.message, "critical timeout",
"record message should match"
);
assert_eq!(
entry.delivered,
vec![false],
"all async channels failed, delivery state should be [false]"
);
drop(buffer_guard);
assert_eq!(
metrics.channel_blocked(),
before_blocked,
"critical level should not increment channel_blocked"
);
assert_eq!(
metrics.logs_dropped(),
before_dropped,
"critical level should not increment logs_dropped"
);
}
#[test]
fn test_with_sanitizer_escapes_newline_in_message() {
let (console_tx, console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let sanitizer = Arc::new(LogSanitizer::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics).with_sanitizer(sanitizer);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::sanitizer", message = "line1\nline2");
});
let received = console_rx.recv().unwrap();
assert!(
received.message.contains("\\n"),
"message should contain escaped newline, got: {:?}",
received.message
);
assert!(
!received.message.contains('\n'),
"message should not contain raw newline"
);
}
#[test]
fn test_without_sanitizer_message_unchanged() {
let (console_tx, console_rx) = bounded(10);
let (async_tx, _async_rx) = bounded(10);
let metrics = Arc::new(Metrics::new());
let layer = LoggerSubscriber::new(console_tx, async_tx, metrics);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::no_sanitizer", message = "plain message");
});
let received = console_rx.recv().unwrap();
assert_eq!(received.message, "plain message");
}
#[test]
fn test_rate_limiter_drops_non_critical_logs() {
let (console_tx, console_rx) = bounded(100);
let (async_tx, _async_rx) = bounded(100);
let metrics = Arc::new(Metrics::new());
let limiter = Arc::new(RateLimiter::new(2));
let layer =
LoggerSubscriber::new(console_tx, async_tx, metrics.clone()).with_rate_limiter(limiter);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
for _ in 0..10 {
tracing::info!(target: "test::rate", message = "flood");
}
});
let mut count = 0;
while console_rx.try_recv().is_ok() {
count += 1;
}
assert!(
count <= 2,
"at most 2 logs should pass rate limiter, got {}",
count
);
assert!(
metrics.logs_dropped() >= 8,
"at least 8 logs should be dropped, got {}",
metrics.logs_dropped()
);
}
#[test]
fn test_rate_limiter_samples_error_on_rejection() {
let (console_tx, console_rx) = bounded(200);
let (async_tx, _async_rx) = bounded(200);
let metrics = Arc::new(Metrics::new());
let limiter = Arc::new(RateLimiter::new(1));
let layer =
LoggerSubscriber::new(console_tx, async_tx, metrics.clone()).with_rate_limiter(limiter);
let registry = tracing_subscriber::registry().with(layer);
with_default(registry, || {
tracing::info!(target: "test::rate", message = "consume token");
for _ in 0..100 {
tracing::error!(target: "test::rate", message = "error flood");
}
});
let mut error_count = 0;
while let Ok(record) = console_rx.try_recv() {
if record.level == "ERROR" {
error_count += 1;
}
}
assert!(
(1..=5).contains(&error_count),
"expected ~1 sampled ERROR through rate limiter, got {}",
error_count
);
}
}
#[cfg(test)]
mod trace_context_tests {
use super::*;
use crossbeam_channel::bounded;
use tracing::subscriber::with_default;
use tracing_subscriber::prelude::*;
type TestSubscriber =
tracing_subscriber::layer::Layered<LoggerSubscriber, tracing_subscriber::Registry>;
fn setup() -> (TestSubscriber, crossbeam_channel::Receiver<Arc<LogRecord>>) {
let (console_tx, console_rx) = bounded(100);
let (async_tx, _async_rx) = bounded(100);
let layer = LoggerSubscriber::new(console_tx, async_tx, Arc::new(Metrics::new()));
(tracing_subscriber::registry().with(layer), console_rx)
}
#[test]
fn test_event_inside_span_gets_trace_and_span_ids() {
let (subscriber, console_rx) = setup();
with_default(subscriber, || {
let span = tracing::info_span!("handler", request = "r-1");
let _guard = span.enter();
tracing::info!(target: "t504", message = "inside span");
tracing::info!(target: "t504", message = "still inside");
});
let r1 = console_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
let r2 = console_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
let (s1, s2) = (
r1.span_id.clone().expect("span_id"),
r2.span_id.clone().expect("span_id"),
);
assert_eq!(s1, s2, "same span must share span_id");
assert_eq!(s1.len(), 16, "span_id must be 16-char hex");
let (t1, t2) = (
r1.trace_id.clone().expect("trace_id"),
r2.trace_id.clone().expect("trace_id"),
);
assert_eq!(t1, t2, "same span must share trace_id");
assert_eq!(t1.len(), 32, "trace_id must be 32-char hex");
assert_ne!(t1, s1, "trace_id must not equal span_id (root derivation)");
}
#[test]
fn test_child_span_shares_root_trace_id() {
let (subscriber, console_rx) = setup();
with_default(subscriber, || {
let root = tracing::info_span!("root");
let _root_guard = root.enter();
tracing::info!(target: "t504", message = "at root");
let child = tracing::info_span!("child");
let _child_guard = child.enter();
tracing::info!(target: "t504", message = "at child");
});
let root = console_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
let child = console_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
let root_trace = root.trace_id.clone().expect("root trace_id");
let child_trace = child.trace_id.clone().expect("child trace_id");
assert_eq!(
root_trace, child_trace,
"child span must inherit the root span's trace_id"
);
assert_ne!(
root.span_id, child.span_id,
"different spans must have different span_ids"
);
}
#[test]
fn test_event_outside_span_has_no_trace_ids() {
let (subscriber, console_rx) = setup();
with_default(subscriber, || {
tracing::info!(target: "t504", message = "no span");
});
let record = console_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
assert!(record.trace_id.is_none(), "no span → trace_id None");
assert!(record.span_id.is_none(), "no span → span_id None");
}
#[test]
fn test_explicit_trace_fields_override_derivation() {
let (subscriber, console_rx) = setup();
with_default(subscriber, || {
let span = tracing::info_span!("otel-ish");
let _guard = span.enter();
tracing::info!(
target: "t504",
message = "explicit",
trace_id = "0af7651916cd43dd8448eb211c80319c",
span_id = "b7ad6b7169203331"
);
});
let record = console_rx
.recv_timeout(std::time::Duration::from_secs(2))
.unwrap();
assert_eq!(
record.trace_id.as_deref(),
Some("0af7651916cd43dd8448eb211c80319c"),
"explicit trace_id field must win (OTel compatibility)"
);
assert_eq!(
record.span_id.as_deref(),
Some("b7ad6b7169203331"),
"explicit span_id field must win (OTel compatibility)"
);
}
}