use std::net::IpAddr;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::Serialize;
pub const LOG_CAP: usize = 10_000;
const NAME_CAP: usize = 253;
const CLIENT_CAP: usize = 45;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum LogAction {
Forwarded = 0,
Cached = 1,
Local = 2,
Blocked = 3,
Nxdomain = 4,
Refused = 5,
Servfail = 6,
}
impl LogAction {
pub fn as_str(self) -> &'static str {
match self {
Self::Forwarded => "forwarded",
Self::Cached => "cached",
Self::Local => "local",
Self::Blocked => "blocked",
Self::Nxdomain => "nxdomain",
Self::Refused => "refused",
Self::Servfail => "servfail",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"forwarded" => Some(Self::Forwarded),
"cached" => Some(Self::Cached),
"local" => Some(Self::Local),
"blocked" => Some(Self::Blocked),
"nxdomain" => Some(Self::Nxdomain),
"refused" => Some(Self::Refused),
"servfail" => Some(Self::Servfail),
_ => None,
}
}
}
pub struct LogEntry {
pub ts_secs: u64,
pub name_buf: [u8; NAME_CAP],
pub name_len: u8, pub client_buf: [u8; CLIENT_CAP],
pub client_len: u8,
pub qtype: u16,
pub action: LogAction,
pub elapsed_ms: u32,
}
impl LogEntry {
pub fn new(
name: &str,
client: &str,
qtype: u16,
action: LogAction,
elapsed_ms: u32,
) -> Self {
let ts_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut name_buf = [0u8; NAME_CAP];
let name_bytes = name.as_bytes();
let name_len = name_bytes.len().min(NAME_CAP) as u8;
name_buf[..name_len as usize].copy_from_slice(&name_bytes[..name_len as usize]);
let mut client_buf = [0u8; CLIENT_CAP];
let client_bytes = client.as_bytes();
let client_len = client_bytes.len().min(CLIENT_CAP) as u8;
client_buf[..client_len as usize].copy_from_slice(&client_bytes[..client_len as usize]);
Self { ts_secs, name_buf, name_len, client_buf, client_len, qtype, action, elapsed_ms }
}
pub fn name(&self) -> &str {
std::str::from_utf8(&self.name_buf[..self.name_len as usize]).unwrap_or("")
}
pub fn client(&self) -> &str {
std::str::from_utf8(&self.client_buf[..self.client_len as usize]).unwrap_or("")
}
}
#[derive(Serialize)]
pub struct LogEntryView {
pub ts: String,
pub name: String,
pub client: String,
pub qtype: u16,
pub action: &'static str,
pub elapsed_ms: u32,
}
pub struct LogQuery {
pub limit: usize, pub page: usize, pub action: Option<LogAction>,
pub client: Option<IpAddr>,
pub since_secs: Option<u64>,
}
pub struct LogBuffer {
slots: Vec<Option<LogEntry>>,
head: usize, count: usize, capacity: usize,
log_client_ip: bool,
}
impl LogBuffer {
fn new_with(capacity: usize, log_client_ip: bool) -> Self {
let cap = capacity.min(LOG_CAP);
let mut slots = Vec::with_capacity(cap);
for _ in 0..cap { slots.push(None); }
Self { slots, head: 0, count: 0, capacity: cap, log_client_ip }
}
pub fn push(&mut self, entry: LogEntry) {
if self.capacity == 0 { return; }
self.slots[self.head] = Some(entry);
self.head = (self.head + 1) % self.capacity;
if self.count < self.capacity { self.count += 1; }
}
pub fn push_query(
&mut self,
name: &str,
client_ip: &std::net::IpAddr,
qtype: u16,
action: LogAction,
elapsed_ms: u32,
) -> String {
let client_str = if self.log_client_ip {
client_ip.to_string()
} else {
"[redacted]".to_string()
};
self.push(LogEntry::new(name, &client_str, qtype, action, elapsed_ms));
client_str
}
pub fn clear(&mut self) -> usize {
let deleted = self.count;
for slot in &mut self.slots { *slot = None; }
self.head = 0;
self.count = 0;
deleted
}
pub fn query(&self, q: &LogQuery) -> (Vec<LogEntryView>, usize) {
let filled = self.count.min(self.capacity);
if filled == 0 { return (vec![], 0); }
let mut matched: Vec<LogEntryView> = Vec::new();
for i in 0..filled {
let idx = (self.head + self.capacity - 1 - i) % self.capacity;
let entry = match &self.slots[idx] {
Some(e) => e,
None => continue,
};
if let Some(a) = q.action {
if entry.action != a { continue; }
}
if let Some(ref c) = q.client {
if entry.client() != c.to_string() { continue; }
}
if let Some(since) = q.since_secs {
if entry.ts_secs < since { continue; }
}
matched.push(LogEntryView {
ts: format_ts(entry.ts_secs),
name: entry.name().to_owned(),
client: entry.client().to_owned(),
qtype: entry.qtype,
action: entry.action.as_str(),
elapsed_ms: entry.elapsed_ms,
});
}
let total = matched.len();
let start = (q.page * q.limit).min(total);
let end = (start + q.limit).min(total);
(matched.drain(start..end).collect(), total)
}
}
pub type SharedLogBuffer = Arc<Mutex<LogBuffer>>;
pub fn new_shared(capacity: usize, log_client_ip: bool) -> SharedLogBuffer {
Arc::new(Mutex::new(LogBuffer::new_with(capacity, log_client_ip)))
}
pub fn format_ts(secs: u64) -> String {
let s = secs % 86400;
let days = secs / 86400;
let hh = s / 3600;
let mm = (s % 3600) / 60;
let ss = s % 60;
let z = days as i64 + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, hh, mm, ss)
}