pub use ::inklog::{InklogConfig, InklogError, LoggerManager};
pub async fn init_inklog_logger() -> Result<LoggerManager, InklogError> {
init_inklog_logger_with_config(InklogConfig::default()).await
}
pub async fn init_inklog_logger_with_config(
config: InklogConfig,
) -> Result<LoggerManager, InklogError> {
LoggerManager::with_config(config).await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[serial_test::serial]
async fn init_inklog_logger_returns_manager() {
let result = init_inklog_logger().await;
assert!(result.is_ok(), "init_inklog_logger should return Ok");
}
#[tokio::test]
#[serial_test::serial]
async fn log_macros_survive_after_inklog_init() {
let _manager = init_inklog_logger()
.await
.expect("init_inklog_logger should succeed");
log::info!("inklog bridge active");
log::warn!("inklog bridge warn");
tracing::info!(target: "limiteron", "tracing via inklog");
}
#[tokio::test]
#[serial_test::serial]
async fn init_inklog_logger_is_idempotent() {
let _first = init_inklog_logger()
.await
.expect("first init should succeed");
let second = init_inklog_logger().await;
assert!(
second.is_ok(),
"second init should still return Ok (install failure downgraded to warn)"
);
}
#[tokio::test]
#[serial_test::serial]
async fn init_inklog_logger_with_config_works() {
let config = InklogConfig::default();
let result = init_inklog_logger_with_config(config).await;
assert!(
result.is_ok(),
"init_inklog_logger_with_config should return Ok"
);
}
}
use std::sync::Arc;
use dashmap::DashMap;
use parking_lot::Mutex;
#[derive(Debug)]
struct SinkBucket {
capacity: u64,
refill_per_sec: f64,
state: Mutex<SinkBucketState>,
}
#[derive(Debug)]
struct SinkBucketState {
tokens: f64,
last_refill: std::time::Instant,
}
impl SinkBucket {
fn new(capacity: u64, refill_per_sec: f64) -> Self {
Self {
capacity,
refill_per_sec,
state: Mutex::new(SinkBucketState {
tokens: capacity as f64,
last_refill: std::time::Instant::now(),
}),
}
}
fn refill(&self) -> f64 {
let mut st = self.state.lock();
let elapsed = st.last_refill.elapsed().as_secs_f64();
if elapsed > 0.0 {
st.tokens = (st.tokens + elapsed * self.refill_per_sec).min(self.capacity as f64);
st.last_refill = std::time::Instant::now();
}
st.tokens
}
fn try_acquire(&self) -> bool {
let mut st = self.state.lock();
let elapsed = st.last_refill.elapsed().as_secs_f64();
if elapsed > 0.0 {
st.tokens = (st.tokens + elapsed * self.refill_per_sec).min(self.capacity as f64);
st.last_refill = std::time::Instant::now();
}
if st.tokens < 1.0 {
return false;
}
st.tokens -= 1.0;
true
}
fn refund(&self) {
let mut st = self.state.lock();
st.tokens = (st.tokens + 1.0).min(self.capacity as f64);
}
fn available(&self) -> u64 {
self.refill() as u64
}
}
pub struct LimiteronSinkRateLimit {
capacity: u64,
refill_per_sec: f64,
buckets: DashMap<String, Arc<SinkBucket>>,
}
impl LimiteronSinkRateLimit {
pub fn new(capacity: u64, refill_per_sec: f64) -> Self {
Self {
capacity,
refill_per_sec,
buckets: DashMap::new(),
}
}
pub fn tracked_targets(&self) -> usize {
self.buckets.len()
}
fn bucket_for(&self, target: &str) -> Arc<SinkBucket> {
if let Some(b) = self.buckets.get(target) {
return b.value().clone();
}
let bucket = Arc::new(SinkBucket::new(self.capacity, self.refill_per_sec));
self.buckets.insert(target.to_string(), bucket.clone());
bucket
}
pub fn available(&self, target: &str) -> u64 {
self.buckets
.get(target)
.map(|b| b.value().available())
.unwrap_or(self.capacity)
}
}
impl ::inklog::SinkRateLimit for LimiteronSinkRateLimit {
fn try_acquire(&self, record: &::inklog::LogRecord) -> bool {
if record.level == "ERROR" {
return true;
}
self.bucket_for(&record.target).try_acquire()
}
fn report(&self, record: &::inklog::LogRecord, outcome: ::inklog::SinkWriteOutcome) {
if outcome == ::inklog::SinkWriteOutcome::Failed {
self.bucket_for(&record.target).refund();
}
}
fn name(&self) -> &str {
"limiteron-sink-rate-limit"
}
}
#[cfg(test)]
mod sink_rate_limit_tests {
use super::*;
use ::inklog::{SinkRateLimit, SinkWriteOutcome};
fn record(level: &str, target: &str) -> ::inklog::LogRecord {
let mut r =
::inklog::LogRecord::new(tracing::Level::INFO, target.to_string(), "msg".to_string());
r.level = level.to_string();
r
}
#[test]
fn test_t617_sink_rate_limit_budget_enforced() {
let lim = LimiteronSinkRateLimit::new(3, 0.0);
assert!(lim.try_acquire(&record("INFO", "app::a")));
assert!(lim.try_acquire(&record("INFO", "app::a")));
assert!(lim.try_acquire(&record("INFO", "app::a")));
assert!(
!lim.try_acquire(&record("INFO", "app::a")),
"预算耗尽必须拒绝"
);
assert_eq!(lim.available("app::a"), 0);
}
#[test]
fn test_t617_sink_rate_limit_error_bypasses() {
let lim = LimiteronSinkRateLimit::new(1, 0.0);
assert!(lim.try_acquire(&record("WARN", "app::b")));
assert!(!lim.try_acquire(&record("WARN", "app::b")));
for _ in 0..5 {
assert!(
lim.try_acquire(&record("ERROR", "app::b")),
"ERROR 必须直通"
);
}
assert_eq!(lim.available("app::b"), 0, "ERROR 直通不消费预算");
}
#[test]
fn test_t617_sink_rate_limit_targets_isolated() {
let lim = LimiteronSinkRateLimit::new(2, 0.0);
assert!(lim.try_acquire(&record("INFO", "mod::x")));
assert!(lim.try_acquire(&record("INFO", "mod::x")));
assert!(!lim.try_acquire(&record("INFO", "mod::x")));
assert!(
lim.try_acquire(&record("INFO", "mod::y")),
"兄弟 target 不受影响"
);
assert_eq!(lim.tracked_targets(), 2);
}
#[test]
fn test_t617_sink_rate_limit_failed_report_refunds() {
let lim = LimiteronSinkRateLimit::new(1, 0.0);
let rec = record("INFO", "app::c");
assert!(lim.try_acquire(&rec));
assert!(!lim.try_acquire(&rec));
lim.report(&rec, SinkWriteOutcome::Failed);
assert_eq!(lim.available("app::c"), 1, "写失败必须归还预算");
assert!(lim.try_acquire(&rec));
lim.report(&rec, SinkWriteOutcome::Written);
assert_eq!(lim.available("app::c"), 0);
}
#[test]
fn test_t617_sink_rate_limit_name() {
let lim = LimiteronSinkRateLimit::new(10, 10.0);
assert_eq!(SinkRateLimit::name(&lim), "limiteron-sink-rate-limit");
}
#[test]
fn test_t617_sink_rate_limit_refills_over_time() {
let lim = LimiteronSinkRateLimit::new(100, 100.0);
let rec = record("INFO", "app::d");
for _ in 0..100 {
assert!(lim.try_acquire(&rec));
}
assert!(!lim.try_acquire(&rec));
std::thread::sleep(std::time::Duration::from_millis(120));
assert!(
lim.available("app::d") >= 5,
"120ms @ 100/s 应至少恢复 ~12 个预算(时钟偏移留裕量)"
);
assert!(lim.try_acquire(&rec));
}
}