编辑
2024-01-19
网络
00
请注意,本文编写于 272 天前,最后修改于 272 天前,其中某些信息可能已经过时。

目录

wmproxy
项目地址
项目中的使用
文件配置
日志的组成部分
注意点
分析Pattern
小结

wmproxy

wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

项目中的使用

目前需要将每条请求数据进入的日志,如access_log,或者项目相关的错误日志error_log记录下来。

以下将介绍项目中如何进行记录并格式化日志的

文件配置

当前需要根据项目中的配置进行相应的初始化,需要用代码将当前的配置进行初始化。

TOML
[http] # 访问列表的写入文件及格式 access_log = "access main debug" # 错误列表的写入文件及格式,错误的第二个是错误等级。 error_log = "error debug" # 日志格式 [http.log_format] main = "{d(%Y-%m-%d %H:%M:%S)} {client_ip} {l} {url} path:{path} query:{query} host:{host} status: {status} {up_status} referer: {referer} user_agent: {user_agent} cookie: {cookie}" [http.log_names] access = "logs/access.log trace" error = "logs/error.log" default = "logs/default.log"

日志的组成部分

日志的组成分为三个部分

  1. access_log及error_log的写入文件、格式及日志等级
  2. log_names日志的别名,包含日志文件及可能包含日志等级,没有等级默认Info
  3. 日志格式,记录日志携带的相关消息,如访问的客户端ip{client_ip}或者访问Url{url}等,遵循Rust的打印结构,用{}里面包含要打印的相关消息

以下是访问信息打印的数据

2023-11-16 15:02:00 127.0.0.1:55922 INFO http://127.0.0.1:82/root/?aaa=1 path:/root/ query:aaa=1 host:127.0.0.1 status: ??? referer: user_agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0 cookie:

注意点

因为access_logerror_log可以在[http]的层级下任意配置,第一步我们需要收集到合适的log_names进行初始化,我们用的是一个HashMap做键值对,防止重复:

RUST
/// http.rs pub fn get_log_names(&self, names: &mut HashMap<String, String>) { self.comm.get_log_names(names); for s in &self.server { s.get_log_names(names); } } /// server.rs pub fn get_log_names(&self, names: &mut HashMap<String, String>) { self.comm.get_log_names(names); for l in &self.location { l.get_log_names(names); } } /// common.rs pub fn get_log_names(&self, names: &mut HashMap<String, String>) { for val in &self.log_names { if !names.contains_key(val.0) { names.insert(val.0.clone(), val.1.clone()); } } }

收集好正确的log文件后,我们需要对其初始化或者重加载,其中重新加载需要拥有上次初始化的Handle那么我们需对基进行存储:

RUST
lazy_static! { /// 用静态变量存储log4rs的Handle static ref LOG4RS_HANDLE: Mutex<Option<log4rs::Handle>> = Mutex::new(None); } /// 尝试初始化, 如果已初始化则重新加载 pub fn try_init_log(option: &ConfigOption) { let log_names = option.get_log_names(); let mut log_config = log4rs::config::Config::builder(); let mut root = Root::builder(); for (name, path) in log_names { let (path, level) = { let vals: Vec<&str> = path.split(' ').collect(); if vals.len() == 1 { (path, Level::Info) } else { ( vals[0].to_string(), Level::from_str(vals[1]).ok().unwrap_or(Level::Info), ) } }; // 设置默认的匹配类型打印时间信息 let parttern = log4rs::encode::pattern::PatternEncoder::new("{d(%Y-%m-%d %H:%M:%S)} {m}{n}"); let appender = FileAppender::builder() .encoder(Box::new(parttern)) .build(path) .unwrap(); if name == "default" { root = root.appender(name.clone()); } log_config = log_config.appender(Appender::builder().build(name.clone(), Box::new(appender))); log_config = log_config.logger( Logger::builder() .appender(name.clone()) // 当前target不在输出到stdout中 .additive(false) .build(name.clone(), level.to_level_filter()), ); } if !option.disable_stdout { let stdout: ConsoleAppender = ConsoleAppender::builder().build(); log_config = log_config.appender(Appender::builder().build("stdout", Box::new(stdout))); root = root.appender("stdout"); } let log_config = log_config.build(root.build(LevelFilter::Info)).unwrap(); // 检查静态变量中是否存在handle可能在多线程中,需加锁 if LOG4RS_HANDLE.lock().unwrap().is_some() { LOG4RS_HANDLE .lock() .unwrap() .as_mut() .unwrap() .set_config(log_config); } else { let handle = log4rs::init_config(log_config).unwrap(); *LOG4RS_HANDLE.lock().unwrap() = Some(handle); } }

我们需要在初始化参数的时候在重新调用该函数,保证新的日志信息能正确的初始化。

下面是将访问日志的数据打印下来:

RUST
/// 记录HTTP的访问数据并将其格式化 pub fn log_acess( log_formats: &HashMap<String, String>, access: &Option<ConfigLog>, req: &Request<RecvStream>, ) { if let Some(access) = access { if let Some(formats) = log_formats.get(&access.format) { // 需要先判断是否该日志已开启, 如果未开启直接写入将浪费性能 if log_enabled!(target: &access.name, access.level) { // 将format转化成pattern会有相当的性能损失, 此处缓存pattern结果 let pw = FORMAT_PATTERN_CACHE.with(|m| { if !m.borrow().contains_key(&**formats) { let p = PatternEncoder::new(formats); m.borrow_mut() .insert(Box::leak(formats.clone().into_boxed_str()), Arc::new(p)); } m.borrow()[&**formats].clone() }); // 将其转化成Record然后进行encode let record = ProxyRecord::new_req(Record::builder().level(Level::Info).build(), req); let mut buf = vec![]; pw.encode(&mut SimpleWriter(&mut buf), &record).unwrap(); log::info!(target: &access.name, "{}", String::from_utf8_lossy(&buf[..])) } } } }

其中缓存pattern的结果性能损失的要求不高,但需要访问速度要高:

RUST
thread_local! { static FORMAT_PATTERN_CACHE: RefCell<HashMap<&'static str, Arc<PatternEncoder>>> = RefCell::new(HashMap::new()); }

加RefCell是因为默认是不可变的,如果有新的数据,需要将其变成可变数据,从而进行缓存。 HashMap中的key用&'static str是可以不必要将一些数据转化成String避免不必要的拷贝。 如果将String变成&'static str那么意味着这段内存将会变成不可回收的数据,意味着内存泄漏,所以我们需要用Box::leak

RUST
Box::leak(formats.clone().into_boxed_str()

HashMap中的value中用Arc,因为我们是一个全部变量,我们要尽量的减少其访问的时间,但是我们又需要持有Pattern,所以我们在这里应用了一个引用计数Arc,拷贝的时候仅仅消耗加减引用计数。

RUST
m.borrow()[&**formats].clone()

分析Pattern

以下代码大部分来自log4rs

RUST
pub struct PatternEncoder { chunks: Vec<Chunk>, pattern: String, }

首先会将一个字符串拆成若干个Chunk信息,

rust
enum Chunk { Text(String), Formatted { chunk: FormattedChunk, params: Parameters, }, Error(String), }

以下用date: {d(%Y-%m-%d %H:%M:%S)} url: {url}{n}做示范,我们在解析这字符串的时候将会得到以下五个部分:

  1. date: 这是一个常量数据也就是Text将原样输出
  2. {d(%Y-%m-%d %H:%M:%S)}将会转化成Formatted::FormattedChunk::Time(String, Timezone),然后根据数组遍历,若为这个,那边将写入时间信息2023-11-16 15:02:00
  3. url:常量,原样输出
  4. {url}将会转成FormattedChunk::Url如果存在Request将从其中获取url地址,若没有则输出???
  5. {N}将会转成FormattedChunk::Newline,将会 根据平台输出换行符。

此时我们的输出只需要进行一次遍历即可O(n),也不必replace等造成字符串的数据重排导致时间的变化。

此外还有额外参数:

  • {client_ip} 客户端IP
  • {url} 访问Url
  • {path} 访问路径,如/user/login
  • {query} 访问请求参数,如user=wmproxy&password=wmproxy
  • {host} 访问Host
  • {referer} 访问的referer
  • {user_agent} 客户端Agent
  • {cookie} 当前访问的cookie

小结

日志在程序中必不可少,那么需要尽可能的高效,所以尽可能的提升日志的效率是必须处理的一环。

点击 [关注][在看][点赞] 是对作者最大的支持

本文作者:问蒙服务框架

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!