use super::{Node, NodeContext, NodeKind, NodeRequest, NodeResponse}; use crate::{config::DynNode, error::ServiceError}; use futures::Future; use log::error; use serde::Deserialize; use std::{path::PathBuf, pin::Pin, sync::Arc, time::SystemTime}; use tokio::{ fs::{File, OpenOptions}, io::{AsyncWriteExt, BufWriter}, sync::RwLock, }; pub struct AccessLogKind; #[derive(Deserialize)] struct AccessLogConfig { file: PathBuf, #[serde(default)] flush: bool, #[serde(default)] reject_on_fail: bool, next: DynNode, } struct AccessLog { config: AccessLogConfig, file: RwLock>>, } impl NodeKind for AccessLogKind { fn name(&self) -> &'static str { "access_log" } fn instanciate(&self, config: serde_yml::Value) -> anyhow::Result> { Ok(Arc::new(AccessLog { config: serde_yml::from_value::(config)?, file: Default::default(), })) } } impl Node for AccessLog { fn handle<'a>( &'a self, context: &'a mut NodeContext, request: NodeRequest, ) -> Pin> + Send + Sync + 'a>> { Box::pin(async move { let mut g = self.file.write().await; let log = match g.as_mut() { Some(r) => r, None => g.insert(BufWriter::new( OpenOptions::new() .append(true) .create(true) .open(&self.config.file) .await?, )), }; let method = request.method().as_str(); let time = SystemTime::UNIX_EPOCH.elapsed().unwrap().as_micros(); let addr = context.addr; let mut res = log .write_all(format!("{time}\t{addr}\t{method}\t{:?}\n", request.uri()).as_bytes()) .await; if self.config.flush && res.is_ok() { res = log.flush().await; } if self.config.reject_on_fail { res? } else if let Err(e) = res { error!("failed to write log: {e:?}") } self.config.next.handle(context, request).await }) } }