use tracing::subscriber::Interest;
use tracing::{Metadata, Subscriber};
use tracing_subscriber::layer::{Context, Layer};
pub trait EventGate: Send + Sync + 'static {
fn allows(&self, meta: &Metadata<'_>) -> bool;
}
pub struct GateLayer {
gates: Vec<Box<dyn EventGate>>,
}
impl GateLayer {
pub fn new(gates: Vec<Box<dyn EventGate>>) -> Option<Self> {
if gates.is_empty() {
return None;
}
Some(Self { gates })
}
}
impl std::fmt::Debug for GateLayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GateLayer")
.field("gates", &self.gates.len())
.finish()
}
}
impl<S: Subscriber> Layer<S> for GateLayer {
fn register_callsite(&self, _meta: &'static Metadata<'static>) -> Interest {
Interest::sometimes()
}
fn enabled(&self, meta: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
if !meta.is_event() {
return true;
}
self.gates.iter().all(|gate| gate.allows(meta))
}
}
pub(super) fn layer_for(
sampling: Option<super::sampling::SampleConfig>,
rate_limit: Option<super::rate_limit::RateLimit>,
) -> Option<GateLayer> {
let mut gates: Vec<Box<dyn EventGate>> = Vec::new();
if let Some(config) = sampling {
gates.push(Box::new(super::sampling::Sampler::new(config)));
}
if let Some(config) = rate_limit {
gates.push(Box::new(super::rate_limit::RateLimiter::new(config)));
}
GateLayer::new(gates)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tracing_subscriber::layer::SubscriberExt as _;
struct ScriptedGate {
verdict: bool,
calls: Arc<AtomicUsize>,
}
impl EventGate for ScriptedGate {
fn allows(&self, _meta: &Metadata<'_>) -> bool {
self.calls.fetch_add(1, Ordering::Relaxed);
self.verdict
}
}
fn scripted(verdict: bool) -> (Box<dyn EventGate>, Arc<AtomicUsize>) {
let calls = Arc::new(AtomicUsize::new(0));
let gate = ScriptedGate {
verdict,
calls: Arc::clone(&calls),
};
(Box::new(gate), calls)
}
#[test]
fn new_returns_none_without_gates() {
let layer = GateLayer::new(Vec::new());
assert!(layer.is_none());
}
#[test]
fn new_returns_a_layer_when_gates_are_present() {
let (gate, _) = scripted(true);
let layer = GateLayer::new(vec![gate]);
assert!(layer.is_some());
}
#[test]
fn a_vetoing_gate_short_circuits_the_rest_of_the_chain() {
let (deny, deny_calls) = scripted(false);
let (allow, allow_calls) = scripted(true);
let layer = GateLayer::new(vec![deny, allow]).expect("two gates");
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || tracing::error!("gated"));
assert_eq!(deny_calls.load(Ordering::Relaxed), 1);
assert_eq!(
allow_calls.load(Ordering::Relaxed),
0,
"a gate after a veto must not be consulted"
);
}
#[test]
fn every_event_re_consults_the_gates() {
let (gate, calls) = scripted(true);
let layer = GateLayer::new(vec![gate]).expect("one gate");
let subscriber = tracing_subscriber::registry().with(layer);
tracing::subscriber::with_default(subscriber, || {
for _ in 0..5 {
tracing::error!("repeated callsite");
}
});
assert_eq!(
calls.load(Ordering::Relaxed),
5,
"the gate's verdict was cached instead of re-evaluated"
);
}
#[test]
fn spans_bypass_the_gates() {
let (deny, calls) = scripted(false);
let layer = GateLayer::new(vec![deny]).expect("one gate");
let subscriber = tracing_subscriber::registry().with(layer);
let is_disabled = tracing::subscriber::with_default(subscriber, || {
tracing::info_span!("survives").is_disabled()
});
assert!(!is_disabled, "a vetoing gate must not disable spans");
assert_eq!(calls.load(Ordering::Relaxed), 0);
}
}