use std::sync::Arc;
use async_trait::async_trait;
use crate::support::io::LogSink;
use crate::support::query::level_rank;
use crate::{InklogError, LogRecord, Metrics};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MiddlewareVerdict {
Continue,
Drop,
}
pub trait RecordMiddleware: Send + Sync {
fn name(&self) -> &str;
fn process(&self, record: &mut LogRecord) -> MiddlewareVerdict;
}
#[derive(Default)]
pub struct MiddlewareChain {
middlewares: Vec<Arc<dyn RecordMiddleware>>,
}
impl std::fmt::Debug for MiddlewareChain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MiddlewareChain")
.field(
"middlewares",
&self
.middlewares
.iter()
.map(|m| m.name())
.collect::<Vec<_>>(),
)
.finish()
}
}
impl MiddlewareChain {
pub fn new() -> Self {
Self::default()
}
pub fn push(mut self, middleware: Arc<dyn RecordMiddleware>) -> Self {
self.middlewares.push(middleware);
self
}
pub fn len(&self) -> usize {
self.middlewares.len()
}
pub fn is_empty(&self) -> bool {
self.middlewares.is_empty()
}
pub fn apply(&self, record: &mut LogRecord) -> bool {
for middleware in &self.middlewares {
if middleware.process(record) == MiddlewareVerdict::Drop {
return false;
}
}
true
}
}
#[derive(Debug, Clone)]
pub struct LevelFilterMiddleware {
min_rank: u8,
}
impl LevelFilterMiddleware {
pub fn new(min_level: &str) -> Result<Self, InklogError> {
let rank = level_rank(min_level);
if rank == u8::MAX {
return Err(InklogError::ConfigError(format!(
"unknown min level '{min_level}' for level-filter middleware; \
valid levels: trace/debug/info/warn/error/fatal"
)));
}
Ok(Self { min_rank: rank })
}
}
impl RecordMiddleware for LevelFilterMiddleware {
fn name(&self) -> &str {
"level-filter"
}
fn process(&self, record: &mut LogRecord) -> MiddlewareVerdict {
let rank = level_rank(&record.level);
if rank == u8::MAX || rank < self.min_rank {
MiddlewareVerdict::Drop
} else {
MiddlewareVerdict::Continue
}
}
}
#[derive(Debug, Clone)]
pub struct EnrichMiddleware {
key: String,
value: serde_json::Value,
}
impl EnrichMiddleware {
pub fn new(key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
}
impl RecordMiddleware for EnrichMiddleware {
fn name(&self) -> &str {
"enrich"
}
fn process(&self, record: &mut LogRecord) -> MiddlewareVerdict {
record.fields.insert(self.key.clone(), self.value.clone());
MiddlewareVerdict::Continue
}
}
pub struct MiddlewareSink {
inner: Arc<dyn LogSink>,
chain: MiddlewareChain,
metrics: Option<Arc<Metrics>>,
}
impl MiddlewareSink {
pub fn new(inner: Arc<dyn LogSink>, chain: MiddlewareChain) -> Self {
Self {
inner,
chain,
metrics: None,
}
}
pub fn with_metrics(mut self, metrics: Arc<Metrics>) -> Self {
self.metrics = Some(metrics);
self
}
pub fn chain(&self) -> &MiddlewareChain {
&self.chain
}
}
#[async_trait]
impl LogSink for MiddlewareSink {
async fn write(&self, record: &LogRecord) -> Result<(), InklogError> {
if self.chain.is_empty() {
return self.inner.write(record).await;
}
let mut record = record.clone();
if !self.chain.apply(&mut record) {
if let Some(ref metrics) = self.metrics {
metrics.inc_logs_dropped();
}
return Ok(());
}
self.inner.write(&record).await
}
async fn flush(&self) -> Result<(), InklogError> {
self.inner.flush().await
}
fn is_healthy(&self) -> bool {
self.inner.is_healthy()
}
async fn shutdown(&self) -> Result<(), InklogError> {
self.inner.shutdown().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use parking_lot::Mutex;
use tracing::Level;
fn record(level: Level, message: &str) -> LogRecord {
LogRecord::new(level, "mw::test".to_string(), message.to_string())
}
struct CollectingSink {
messages: Mutex<Vec<String>>,
}
#[async_trait]
impl LogSink for CollectingSink {
async fn write(&self, record: &LogRecord) -> Result<(), InklogError> {
self.messages.lock().push(record.message.clone());
Ok(())
}
async fn flush(&self) -> Result<(), InklogError> {
Ok(())
}
async fn shutdown(&self) -> Result<(), InklogError> {
Ok(())
}
}
struct KeywordDropMiddleware {
keyword: String,
}
impl RecordMiddleware for KeywordDropMiddleware {
fn name(&self) -> &str {
"keyword-drop"
}
fn process(&self, record: &mut LogRecord) -> MiddlewareVerdict {
if record.message.contains(&self.keyword) {
MiddlewareVerdict::Drop
} else {
MiddlewareVerdict::Continue
}
}
}
#[test]
fn test_level_filter_middleware() {
let mw = LevelFilterMiddleware::new("warn").expect("valid level");
assert_eq!(
mw.process(&mut record(Level::ERROR, "x")),
MiddlewareVerdict::Continue
);
assert_eq!(
mw.process(&mut record(Level::WARN, "x")),
MiddlewareVerdict::Continue
);
assert_eq!(
mw.process(&mut record(Level::INFO, "x")),
MiddlewareVerdict::Drop
);
}
#[test]
fn test_level_filter_middleware_rejects_unknown_level() {
assert!(LevelFilterMiddleware::new("foobar").is_err());
assert!(LevelFilterMiddleware::new("").is_err());
assert!(LevelFilterMiddleware::new("WARN").is_ok());
}
#[test]
fn test_chain_applies_in_order_and_short_circuits() {
let chain = MiddlewareChain::new()
.push(Arc::new(KeywordDropMiddleware {
keyword: "secret".to_string(),
}))
.push(Arc::new(EnrichMiddleware::new("cluster", "prod-1")));
let mut dropped = record(Level::INFO, "has secret inside");
assert!(!chain.apply(&mut dropped));
assert!(
!dropped.fields.contains_key("cluster"),
"drop must short-circuit"
);
let mut kept = record(Level::INFO, "plain message");
assert!(chain.apply(&mut kept));
assert_eq!(kept.fields.get("cluster").unwrap(), "prod-1");
assert_eq!(chain.len(), 2);
}
#[tokio::test]
async fn test_middleware_sink_filters_and_enriches() {
let inner = Arc::new(CollectingSink {
messages: Mutex::new(Vec::new()),
});
let metrics = Arc::new(Metrics::new());
let sink = Arc::new(
MiddlewareSink::new(
inner.clone() as Arc<dyn LogSink>,
MiddlewareChain::new()
.push(Arc::new(
LevelFilterMiddleware::new("warn").expect("valid level"),
))
.push(Arc::new(EnrichMiddleware::new("tenant", "acme"))),
)
.with_metrics(metrics.clone()),
);
sink.write(&record(Level::ERROR, "keep me")).await.unwrap();
sink.write(&record(Level::INFO, "drop me")).await.unwrap();
let messages = inner.messages.lock();
assert_eq!(
messages.len(),
1,
"filtered record must not reach inner sink"
);
drop(messages);
assert_eq!(
metrics.logs_dropped(),
1,
"filtered record counts as dropped"
);
}
#[tokio::test]
async fn test_middleware_sink_health_and_shutdown_forwarded() {
struct HealthSink {
healthy: std::sync::atomic::AtomicBool,
}
#[async_trait]
impl LogSink for HealthSink {
async fn write(&self, _r: &LogRecord) -> Result<(), InklogError> {
Ok(())
}
async fn flush(&self) -> Result<(), InklogError> {
Ok(())
}
fn is_healthy(&self) -> bool {
self.healthy.load(std::sync::atomic::Ordering::Relaxed)
}
async fn shutdown(&self) -> Result<(), InklogError> {
Ok(())
}
}
let inner = Arc::new(HealthSink {
healthy: std::sync::atomic::AtomicBool::new(true),
});
let sink = MiddlewareSink::new(inner.clone() as Arc<dyn LogSink>, MiddlewareChain::new());
assert!(sink.is_healthy());
inner
.healthy
.store(false, std::sync::atomic::Ordering::Relaxed);
assert!(!sink.is_healthy(), "health must be forwarded");
}
}