use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;
use salvo::{Depot, FlowCtrl, Request, Response, async_trait};
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use tracing::info;
#[allow(clippy::too_many_arguments)]
fn format_log_line(
format: &str,
client_addr: &std::net::SocketAddr,
method: &str,
path: &str,
path_and_query: &str,
status: u16,
latency: std::time::Duration,
headers: &http::HeaderMap,
content_length: Option<u64>,
) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut s = secs;
let sec = s % 60;
s /= 60;
let min = s % 60;
s /= 60;
let hour = s % 24;
s /= 24;
let mut year = 1970u32;
loop {
let days_in_year = if year.is_multiple_of(4)
&& (!year.is_multiple_of(100) || year.is_multiple_of(400))
{
366
} else {
365
};
if s < days_in_year {
break;
}
s -= days_in_year;
year += 1;
}
let leap =
year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400));
let days_in_month: [u64; 12] = [
31,
if leap { 29u64 } else { 28u64 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut month = 0u32;
for (i, &dim) in days_in_month.iter().enumerate() {
if s < dim {
month = i as u32 + 1;
break;
}
s -= dim;
}
let day = s + 1;
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
year, month, day, hour, min, sec
)
};
let host = headers
.get(http::header::HOST)
.and_then(|v| v.to_str().ok())
.unwrap_or("-");
let user_agent = headers
.get(http::header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.unwrap_or("-");
let content_length_str = content_length
.map(|n| n.to_string())
.unwrap_or_else(|| "-".to_string());
let mut result = format.to_string();
result = result.replace("{client_ip}", &client_addr.ip().to_string());
result = result.replace("{client_port}", &client_addr.port().to_string());
result = result.replace("{method}", method);
result = result.replace("{path}", path);
result = result.replace("{path_and_query}", path_and_query);
result = result.replace("{status}", &status.to_string());
result = result.replace("{latency_ms}", &latency.as_millis().to_string());
result = result.replace("{host}", host);
result = result.replace("{timestamp}", ×tamp);
result = result.replace("{content_length}", &content_length_str);
result = result.replace("{user_agent}", user_agent);
while let Some(start) = result.find("{header:") {
let end = match result[start..].find('}') {
Some(e) => start + e,
None => break,
};
let placeholder = &result[start..=end];
let header_name = &placeholder[8..placeholder.len() - 1];
let value = headers
.get(header_name)
.and_then(|v| v.to_str().ok())
.unwrap_or("-");
result = result.replace(placeholder, value);
}
result
}
const DEFAULT_FORMAT: &str =
r#"{client_ip} - [{timestamp}] "{method} {path}" {status} {latency_ms}ms"#;
pub struct RotatingLogWriter {
path: PathBuf,
writer: tokio::io::BufWriter<tokio::fs::File>,
current_size: u64,
max_size: Option<u64>,
max_keep: Option<usize>,
}
impl RotatingLogWriter {
pub async fn open(
path: PathBuf,
max_size: Option<u64>,
max_keep: Option<usize>,
) -> std::io::Result<Self> {
let file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await?;
let current_size = file.metadata().await?.len();
Ok(Self {
path,
writer: tokio::io::BufWriter::new(file),
current_size,
max_size,
max_keep,
})
}
pub async fn write(&mut self, data: &[u8]) -> std::io::Result<()> {
if let Some(max) = self.max_size
&& self.current_size + data.len() as u64 > max
{
self.rotate().await?;
}
self.writer.write_all(data).await?;
self.current_size += data.len() as u64;
Ok(())
}
pub async fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush().await
}
async fn rotate(&mut self) -> std::io::Result<()> {
self.writer.flush().await?;
let keep = self.max_keep.unwrap_or(usize::MAX);
if keep > 0 {
let mut highest = 0usize;
for n in 1..=keep {
let candidate = rotated_path(&self.path, n);
if candidate.exists() {
highest = n;
} else {
break;
}
}
for n in (1..=highest).rev() {
let src = rotated_path(&self.path, n);
if n + 1 > keep {
let _ = std::fs::remove_file(&src);
} else {
let dst = rotated_path(&self.path, n + 1);
let _ = std::fs::rename(&src, &dst);
}
}
if keep >= 1 {
let dst = rotated_path(&self.path, 1);
let _ = std::fs::rename(&self.path, &dst);
}
}
let new_file = tokio::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&self.path)
.await?;
self.writer = tokio::io::BufWriter::new(new_file);
self.current_size = 0;
Ok(())
}
}
fn rotated_path(base: &Path, n: usize) -> PathBuf {
let mut s = base.as_os_str().to_owned();
s.push(format!(".{}", n));
PathBuf::from(s)
}
pub struct LoggingHoop {
log_file: Option<Arc<Mutex<RotatingLogWriter>>>,
_error_log_file: Option<Arc<Mutex<RotatingLogWriter>>>,
format: Option<String>,
}
impl LoggingHoop {
pub fn new() -> Self {
Self {
log_file: None,
_error_log_file: None,
format: None,
}
}
pub fn with_rotating_writer(
writer: Arc<Mutex<RotatingLogWriter>>,
format: Option<String>,
) -> Self {
Self {
log_file: Some(writer),
_error_log_file: None,
format,
}
}
pub fn with_files(
access_writer: Arc<Mutex<RotatingLogWriter>>,
error_writer: Arc<Mutex<RotatingLogWriter>>,
format: Option<String>,
) -> Self {
Self {
log_file: Some(access_writer),
_error_log_file: Some(error_writer),
format,
}
}
pub async fn with_file_path(path: PathBuf, format: Option<String>) -> std::io::Result<Self> {
let writer = RotatingLogWriter::open(path, None, None).await?;
Ok(Self {
log_file: Some(Arc::new(Mutex::new(writer))),
_error_log_file: None,
format,
})
}
}
impl Default for LoggingHoop {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl salvo::Handler for LoggingHoop {
async fn handle(
&self,
req: &mut Request,
depot: &mut Depot,
res: &mut Response,
ctrl: &mut FlowCtrl,
) {
let method = req.method().clone();
let path = req.uri().path().to_string();
let path_and_query = req
.uri()
.path_and_query()
.map(|pq| pq.as_str().to_string())
.unwrap_or_else(|| path.clone());
let client = super::client_addr(req);
let req_headers = req.headers().clone();
let start = Instant::now();
ctrl.call_next(req, depot, res).await;
let elapsed = start.elapsed();
let status = res.status_code.map(|s| s.as_u16()).unwrap_or(200);
let content_length = res
.headers()
.get(http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok());
info!(
client = %client,
method = %method,
path = %path,
status = status,
latency_ms = elapsed.as_millis() as u64,
"request handled"
);
if let Some(ref file) = self.log_file {
let fmt = self.format.as_deref().unwrap_or(DEFAULT_FORMAT);
let line = format_log_line(
fmt,
&client,
method.as_str(),
&path,
&path_and_query,
status,
elapsed,
&req_headers,
content_length,
);
let mut writer = file.lock().await;
let _ = writer.write(line.as_bytes()).await;
let _ = writer.write(b"\n").await;
let _ = writer.flush().await;
}
}
}