Skip to main content
Glama
audit.rs5.21 kB
use anyhow::Result; use chrono::Utc; use serde::Serialize; use sha2::{Digest, Sha256}; use std::fs::{self, File}; use std::io::{BufRead, BufReader, BufWriter, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; #[derive(Clone)] pub struct AuditLogger { inner: Arc<std::sync::Mutex<AuditState>>, } struct AuditState { path: PathBuf, max_bytes: u64, max_files: usize, prev_hash: Option<String>, writer: BufWriter<File>, } #[derive(Serialize)] struct AuditRecord<'a> { ts: String, event: &'a str, outcome: &'a str, request_id: Option<&'a str>, path: Option<&'a str>, method: Option<&'a str>, status: Option<u16>, client_ip: Option<&'a str>, detail: Option<&'a str>, prev_hash: Option<&'a str>, hash: String, } impl AuditLogger { pub fn new(path: PathBuf, max_bytes: u64, max_files: usize) -> Result<Self> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let (writer, prev_hash) = open_with_prev_hash(&path)?; Ok(Self { inner: Arc::new(std::sync::Mutex::new(AuditState { path, max_bytes, max_files: max_files.max(1), prev_hash, writer, })), }) } pub fn log( &self, event: &str, outcome: &str, request_id: Option<&str>, path: Option<&str>, method: Option<&str>, status: Option<u16>, client_ip: Option<&str>, detail: Option<&str>, ) { if let Err(err) = self.write_record( event, outcome, request_id, path, method, status, client_ip, detail, ) { tracing::warn!(target: "docdexd", error = ?err, "failed to write audit log"); } } fn write_record( &self, event: &str, outcome: &str, request_id: Option<&str>, path: Option<&str>, method: Option<&str>, status: Option<u16>, client_ip: Option<&str>, detail: Option<&str>, ) -> Result<()> { let mut guard = self.inner.lock().unwrap(); guard.rotate_if_needed()?; let prev_hash = guard.prev_hash.clone(); let ts = Utc::now().to_rfc3339(); let mut record = AuditRecord { ts, event, outcome, request_id, path, method, status, client_ip, detail, prev_hash: prev_hash.as_deref(), hash: String::new(), }; let serialized = serde_json::to_vec(&record)?; let digest = Sha256::digest(&serialized); record.hash = hex::encode(digest); let line = serde_json::to_vec(&record)?; guard.writer.write_all(&line)?; guard.writer.write_all(b"\n")?; guard.writer.flush()?; guard.prev_hash = Some(record.hash.clone()); Ok(()) } } fn open_with_prev_hash(path: &Path) -> Result<(BufWriter<File>, Option<String>)> { let file = File::options() .create(true) .append(true) .read(true) .open(path)?; let prev_hash = last_hash(path)?; Ok((BufWriter::new(file), prev_hash)) } fn last_hash(path: &Path) -> Result<Option<String>> { let file = match File::open(path) { Ok(file) => file, Err(_) => return Ok(None), }; let reader = BufReader::new(file); let mut last = None; for line in reader .lines() .filter_map(|l| l.ok()) .filter(|l| !l.is_empty()) { last = Some(line); } if let Some(line) = last { if let Ok(json) = serde_json::from_str::<serde_json::Value>(&line) { if let Some(hash) = json.get("hash").and_then(|v| v.as_str()) { return Ok(Some(hash.to_string())); } } } Ok(None) } impl AuditState { fn rotate_if_needed(&mut self) -> Result<()> { let len = self.path.metadata().map(|m| m.len()).unwrap_or(0); if len < self.max_bytes { return Ok(()); } self.writer.flush()?; self.rotate_files()?; let (writer, _) = open_with_prev_hash(&self.path)?; self.writer = writer; Ok(()) } fn rotate_files(&self) -> Result<()> { for idx in (1..self.max_files).rev() { let old = self.rotated_name(idx); let new = self.rotated_name(idx + 1); if old.exists() { let _ = fs::remove_file(&new); fs::rename(&old, &new)?; } } let first = self.rotated_name(1); let _ = fs::remove_file(&first); fs::rename(&self.path, &first)?; Ok(()) } fn rotated_name(&self, idx: usize) -> PathBuf { let mut name = self .path .file_name() .map(|f| f.to_string_lossy().to_string()) .unwrap_or_else(|| "audit.log".to_string()); name.push('.'); name.push_str(&idx.to_string()); self.path .parent() .map(|p| p.join(&name)) .unwrap_or_else(|| PathBuf::from(&name)) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bekirdag/docdex'

If you have feedback or need assistance with the MCP directory API, please join our Discord server