1use std::fmt;
2use std::fs::OpenOptions;
3use std::io::{BufWriter, Write};
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Deserializer, Serialize};
9
10use crate::error::{Result, ServerError};
11
12pub trait AuditLogSink: Send + Sync + 'static {
14 fn log(&self, entry: &AuditLogEntry);
16 fn flush(&self) -> Result<()>;
18}
19
20#[derive(Clone, Default)]
22pub enum AuditLogOutput {
23 #[default]
24 Stdout,
25 File {
26 path: PathBuf,
27 },
28 Custom(Arc<dyn AuditLogSink>),
29}
30
31impl fmt::Debug for AuditLogOutput {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 AuditLogOutput::Stdout => f.debug_tuple("Stdout").finish(),
35 AuditLogOutput::File { path } => f.debug_struct("File").field("path", path).finish(),
36 AuditLogOutput::Custom(_) => f.debug_tuple("Custom").finish(),
37 }
38 }
39}
40
41#[derive(Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43enum AuditLogOutputConfig {
44 Stdout,
45 File { path: PathBuf },
46}
47
48impl<'de> Deserialize<'de> for AuditLogOutput {
49 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
50 where
51 D: Deserializer<'de>,
52 {
53 let config = AuditLogOutputConfig::deserialize(deserializer)?;
54 Ok(match config {
55 AuditLogOutputConfig::Stdout => Self::Stdout,
56 AuditLogOutputConfig::File { path } => Self::File { path },
57 })
58 }
59}
60
61impl AuditLogOutput {
62 fn into_sink(self) -> Result<Arc<dyn AuditLogSink>> {
63 match self {
64 AuditLogOutput::Stdout => Ok(Arc::new(StdoutAuditSink::default())),
65 AuditLogOutput::File { path } => Ok(Arc::new(FileAuditSink::new(path)?)),
66 AuditLogOutput::Custom(sink) => Ok(sink),
67 }
68 }
69}
70
71#[derive(Debug, Serialize)]
73pub struct AuditLogEntry {
74 pub event_type: AuditEventType,
75 pub actor: Option<String>,
76 pub target: String,
77 pub correlation_id: String,
78 pub timestamp: DateTime<Utc>,
79 pub details: serde_json::Value,
80}
81
82#[derive(Debug, Serialize)]
83#[serde(rename_all = "snake_case")]
84pub enum AuditEventType {
85 DdlExecute,
86 ConfigChange,
87 AuthFailure,
88}
89
90#[derive(Clone)]
92pub struct AuditLogger {
93 sink: Arc<dyn AuditLogSink>,
94}
95
96impl AuditLogger {
97 pub fn new(output: AuditLogOutput) -> Result<Self> {
99 Ok(Self {
100 sink: output.into_sink()?,
101 })
102 }
103
104 pub fn log(&self, entry: AuditLogEntry) {
106 self.sink.log(&entry);
107 }
108
109 pub fn log_ddl(&self, sql: &str, actor: Option<&str>, correlation_id: &str) {
111 let entry = AuditLogEntry {
112 event_type: AuditEventType::DdlExecute,
113 actor: actor.map(|v| v.to_string()),
114 target: "ddl".to_string(),
115 correlation_id: correlation_id.to_string(),
116 timestamp: Utc::now(),
117 details: serde_json::json!({ "sql": sql }),
118 };
119 self.log(entry);
120 }
121
122 pub fn log_config_change(&self, key: &str, old: &str, new: &str, correlation_id: &str) {
124 let entry = AuditLogEntry {
125 event_type: AuditEventType::ConfigChange,
126 actor: None,
127 target: key.to_string(),
128 correlation_id: correlation_id.to_string(),
129 timestamp: Utc::now(),
130 details: serde_json::json!({ "old": old, "new": new }),
131 };
132 self.log(entry);
133 }
134
135 pub fn flush(&self) -> Result<()> {
137 self.sink.flush()
138 }
139}
140
141#[derive(Default)]
142struct StdoutAuditSink {
143 buffer: Mutex<()>,
144}
145
146impl AuditLogSink for StdoutAuditSink {
147 fn log(&self, entry: &AuditLogEntry) {
148 let _guard = self.buffer.lock().ok();
149 if let Ok(line) = serde_json::to_string(entry) {
150 println!("{line}");
151 }
152 }
153
154 fn flush(&self) -> Result<()> {
155 Ok(())
156 }
157}
158
159struct FileAuditSink {
160 writer: Mutex<BufWriter<std::fs::File>>,
161}
162
163impl FileAuditSink {
164 fn new(path: PathBuf) -> Result<Self> {
165 let file = OpenOptions::new()
166 .create(true)
167 .append(true)
168 .open(&path)
169 .map_err(ServerError::Io)?;
170 Ok(Self {
171 writer: Mutex::new(BufWriter::new(file)),
172 })
173 }
174}
175
176impl AuditLogSink for FileAuditSink {
177 fn log(&self, entry: &AuditLogEntry) {
178 let Ok(mut writer) = self.writer.lock() else {
179 return;
180 };
181 if let Ok(line) = serde_json::to_string(entry) {
182 let _ = writeln!(writer, "{line}");
183 }
184 }
185
186 fn flush(&self) -> Result<()> {
187 let mut writer = self
188 .writer
189 .lock()
190 .map_err(|_| ServerError::Internal("audit log lock poisoned".into()))?;
191 writer.flush().map_err(ServerError::Io)
192 }
193}