use std::{
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use miette::Result;
use tracing::{debug, warn};
use uuid::Uuid;
use crate::repl::ReplState;
#[cfg(feature = "cli")]
pub mod cli;
pub mod compact;
pub mod history;
mod legacy;
mod lock;
pub mod paths;
pub mod read;
pub mod record;
mod tailscale;
pub mod tools;
pub mod verify;
mod writer;
pub use history::RecallSet;
pub use read::{Entry, Range, Reader, Stored};
pub use record::{QuerySource, Record, RecordKind};
pub use tools::{ExportOptions, QueryOptions, export_audit_entries};
pub use verify::{VerifyReport, verify};
pub use writer::Writer;
#[derive(Debug)]
pub struct Audit {
writer: Writer,
recall: RecallSet,
repl_state: Arc<Mutex<ReplState>>,
}
impl Audit {
pub fn open(dir: impl AsRef<Path>, repl_state: Arc<Mutex<ReplState>>) -> Result<Self> {
let dir = dir.as_ref();
debug!(?dir, "opening audit log");
if let Err(err) = paths::create_dir(dir) {
warn!(?err, ?dir, "cannot prepare the audit directory");
}
match lock::Lock::try_directory(dir) {
Ok(Some(lock)) => {
if let Err(err) = legacy::import(dir, &lock) {
warn!(?err, "could not import the legacy audit store");
}
}
Ok(None) => debug!("another process holds the audit directory"),
Err(err) => warn!(?err, "could not take the audit directory lock"),
}
let recall = RecallSet::build(dir);
compact::spawn(dir.to_path_buf());
Ok(Self {
writer: Writer::new(dir),
recall,
repl_state,
})
}
pub fn open_bare(dir: impl AsRef<Path>) -> Result<Self> {
Self::open(dir, Arc::new(Mutex::new(ReplState::new())))
}
pub fn instance(&self) -> Uuid {
self.writer.instance()
}
pub fn add_entry(&mut self, query: String) -> Result<()> {
self.record(query, QuerySource::Typed);
Ok(())
}
pub fn record(&mut self, query: String, source: QuerySource) {
let context = self.context();
if let Some(ots) = &context.ots {
self.recall.push_supervisor(ots.clone());
}
if source.is_recallable() {
self.recall.push(query.clone());
}
self.writer.query(&context, query, source);
}
pub fn supervisors(&self) -> &[String] {
self.recall.supervisors()
}
fn context(&self) -> writer::Context {
let state = self.repl_state.lock().unwrap();
writer::Context {
sys_user: state.sys_user.clone(),
db_user: state.db_user.clone(),
writemode: state.write_mode,
ots: state.ots.clone(),
}
}
}
pub fn default_path() -> Result<PathBuf> {
paths::default_dir()
}
pub fn default_path_for_help() -> String {
paths::default_dir_for_help()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_session_records_and_recalls_what_it_typed() {
let dir = tempfile::tempdir().unwrap();
let mut audit = Audit::open_bare(dir.path()).unwrap();
audit.add_entry("select 1;".into()).unwrap();
audit.add_entry("select 2;".into()).unwrap();
assert_eq!(audit.recall.len(), 2);
assert_eq!(audit.recall.get(0), Some("select 1;"));
assert_eq!(audit.recall.get(1), Some("select 2;"));
}
#[test]
fn statements_a_file_ran_are_recorded_but_not_recalled() {
let dir = tempfile::tempdir().unwrap();
let mut audit = Audit::open_bare(dir.path()).unwrap();
audit.record("\\i fixups.sql".into(), QuerySource::Typed);
audit.record(
"update patients;".into(),
QuerySource::Include {
path: "/tmp/fixups.sql".into(),
},
);
let instance = audit.instance();
drop(audit);
let logged: Vec<_> = Reader::open(dir.path())
.unwrap()
.entries()
.map(|e| e.query)
.collect();
assert_eq!(logged, vec!["\\i fixups.sql", "update patients;"]);
assert_eq!(RecallSet::build(dir.path()).len(), 1);
assert!(verify(dir.path()).unwrap().holds());
assert_eq!(verify(dir.path()).unwrap().sessions[0].instance, instance);
}
#[test]
fn the_session_that_ran_a_statement_recalls_it_as_a_later_one_would() {
let dir = tempfile::tempdir().unwrap();
let huge = "x".repeat(history::RECALL_CUTOFF + 1);
let mut audit = Audit::open_bare(dir.path()).unwrap();
audit.add_entry("before;".into()).unwrap();
audit.add_entry(huge.clone()).unwrap();
audit.add_entry("after;".into()).unwrap();
let in_session: Vec<_> = (0..audit.recall.len())
.map(|i| audit.recall.get(i).unwrap().to_string())
.collect();
assert_eq!(in_session, vec!["before;", "after;"]);
drop(audit);
let later = RecallSet::build(dir.path());
let afterwards: Vec<_> = (0..later.len())
.map(|i| later.get(i).unwrap().to_string())
.collect();
assert_eq!(in_session, afterwards);
let logged: Vec<_> = Reader::open(dir.path())
.unwrap()
.entries()
.map(|e| e.query)
.collect();
assert!(logged.contains(&huge));
}
#[test]
fn a_supervisor_named_this_session_is_offered_at_the_next_prompt() {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(Mutex::new(ReplState::new()));
let mut audit = Audit::open(dir.path(), Arc::clone(&state)).unwrap();
state.lock().unwrap().write_mode = true;
state.lock().unwrap().ots = Some("Carol".into());
audit.add_entry("update patients;".into()).unwrap();
assert_eq!(audit.supervisors(), &["Carol".to_string()]);
}
#[test]
fn a_new_session_recalls_what_earlier_ones_recorded() {
let dir = tempfile::tempdir().unwrap();
let mut first = Audit::open_bare(dir.path()).unwrap();
first.add_entry("from the first;".into()).unwrap();
drop(first);
let second = Audit::open_bare(dir.path()).unwrap();
assert_eq!(second.recall.get(0), Some("from the first;"));
}
#[test]
fn a_session_does_not_see_what_a_concurrent_one_runs() {
let dir = tempfile::tempdir().unwrap();
let mut mine = Audit::open_bare(dir.path()).unwrap();
mine.add_entry("mine;".into()).unwrap();
let mut theirs = Audit::open_bare(dir.path()).unwrap();
theirs.add_entry("theirs;".into()).unwrap();
assert_eq!(mine.recall.len(), 1);
assert_eq!(mine.recall.get(0), Some("mine;"));
}
#[test]
fn a_new_session_sees_records_from_one_that_was_live_at_the_time() {
let dir = tempfile::tempdir().unwrap();
let mut live = Audit::open_bare(dir.path()).unwrap();
live.add_entry("from the live one;".into()).unwrap();
let later = Audit::open_bare(dir.path()).unwrap();
assert_eq!(later.recall.get(0), Some("from the live one;"));
}
}