28. 干货系列从零用Rust编写正反向代理,项目日志的源码实现
wmproxy
wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子
项目地址
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
项目中的使用
目前需要将每条请求数据进入的日志,如
access_log,或者项目相关的错误日志error_log记录下来。
以下将介绍项目中如何进行记录并格式化日志的
文件配置
当前需要根据项目中的配置进行相应的初始化,需要用代码将当前的配置进行初始化。
[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"
日志的组成部分
日志的组成分为三个部分
- access_log及error_log的写入文件、格式及日志等级
- log_names日志的别名,包含日志文件及可能包含日志等级,没有等级默认Info
- 日志格式,记录日志携带的相关消息,如访问的客户端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_log及error_log可以在[http]的层级下任意配置,第一步我们需要收集到合适的log_names进行初始化,我们用的是一个HashMap做键值对,防止重复:
/// 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那么我们需对基进行存储:
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);
}
}
我们需要在初始化参数的时候在重新调用该函数,保证新的日志信息能正确的初始化。
下面是将访问日志的数据打印下来:
/// 记录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的结果性能损失的要求不高,但需要访问速度要高:
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
Box::leak(formats.clone().into_boxed_str()
HashMap中的value中用Arc,因为我们是一个全部变量,我们要尽量的减少其访问的时间,但是我们又需要持有Pattern,所以我们在这里应用了一个引用计数Arc,拷贝的时候仅仅消耗加减引用计数。
m.borrow()[&**formats].clone()
分析Pattern
以下代码大部分来自log4rs
pub struct PatternEncoder {
chunks: Vec<Chunk>,
pattern: String,
}
首先会将一个字符串拆成若干个Chunk信息,
enum Chunk {
Text(String),
Formatted {
chunk: FormattedChunk,
params: Parameters,
},
Error(String),
}
以下用date: {d(%Y-%m-%d %H:%M:%S)} url: {url}{n}做示范,我们在解析这字符串的时候将会得到以下五个部分:
date:这是一个常量数据也就是Text将原样输出{d(%Y-%m-%d %H:%M:%S)}将会转化成Formatted::FormattedChunk::Time(String, Timezone),然后根据数组遍历,若为这个,那边将写入时间信息2023-11-16 15:02:00url:常量,原样输出{url}将会转成FormattedChunk::Url如果存在Request将从其中获取url地址,若没有则输出???{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
小结
日志在程序中必不可少,那么需要尽可能的高效,所以尽可能的提升日志的效率是必须处理的一环。
点击 [关注],[在看],[点赞] 是对作者最大的支持
28. 干货系列从零用Rust编写正反向代理,项目日志的源码实现的更多相关文章
- arcgis api 3.x for js 共享干货系列之二自定义 Navigation 控件样式风格(附源码下载)
0.内容概览 自定义 Navigation 控件样式风格 源码下载 1.内容讲解 arcgis api 3.x for js 默认的Navigation控件样式风格如下图:这样的风格不能说不好,各有各 ...
- PHP扩展编写、PHP扩展调试、VLD源码分析、基于嵌入式Embed SAPI实现opcode查看
catalogue . 编译PHP源码 . 扩展结构.优缺点 . 使用PHP原生扩展框架wizard ext_skel编写扩展 . 编译安装VLD . Debug调试VLD . VLD源码分析 . 嵌 ...
- 《手把手教你》系列技巧篇(六)-java+ selenium自动化测试-阅读selenium源码(详细教程)
1.简介 前面几篇基础系列文章,足够你迈进了Selenium门槛,再不济你也至少知道如何写你第一个基于Java的Selenium自动化测试脚本.接下来宏哥介绍Selenium技巧篇,主要是介绍一些常用 ...
- MVVM架构~knockoutjs系列之表单添加(验证)与列表操作源码开放
返回目录 本文章应该是knockoutjs系列的最后一篇了,前几篇中主要讲一些基础知识,这一讲主要是一个实际的例子,对于一个对象的添加与编辑功能的实现,并将项目源代码公开了,共大家一起学习! knoc ...
- arcgis api 3.x for js 入门开发系列批量叠加 zip 压缩 SHP 图层优化篇(附源码下载)
前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 3.x for js:esri 官网 api,里面详细的介绍 arcgis api 3.x 各个类 ...
- Springboot系列:Springboot与Thymeleaf模板引擎整合基础教程(附源码)
前言 由于在开发My Blog项目时使用了大量的技术整合,针对于部分框架的使用和整合的流程没有做详细的介绍和记录,导致有些朋友用起来有些吃力,因此打算在接下来的时间里做一些基础整合的介绍,当然,可能也 ...
- vue系列---理解Vue中的computed,watch,methods的区别及源码实现(六)
_ 阅读目录 一. 理解Vue中的computed用法 二:computed 和 methods的区别? 三:Vue中的watch的用法 四:computed的基本原理及源码实现 回到顶部 一. 理解 ...
- leaflet-webpack 入门开发系列二加载不同在线地图切换显示(附源码下载)
前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...
- 深入浅出Mybatis系列三-配置详解之properties与environments(mybatis源码篇)
注:本文转载自南轲梦 注:博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 上篇文章<深入浅出Mybatis系列(二)---配置简介(mybatis源码篇 ...
- openlayers4 入门开发系列之批量叠加 zip 压缩 SHP 图层篇(附源码下载)
前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...
随机推荐
- Selenium+2Captcha 自动化+验证码识别实战
本文深入探讨了使用Selenium库进行网页自动化操作,并结合2Captcha服务实现ReCAPTCHA验证码的破解.内容涵盖Selenium的基础知识.验证码的分类.2Captcha服务的使用,以及 ...
- 文心一言 VS 讯飞星火 VS chatgpt (72)-- 算法导论7.1 3题
三.请简要地证明:在规模为 n 的子数组上,PARTITION 的时间复杂度为 Θ(n). 文心一言: 首先,我们需要明确PARTITION函数的具体定义.PARTITION函数通常用于快速排序算法中 ...
- 【go笔记】TCP编程
前言 TCP服务端的处理流程 监听端口 接收客户端请求建立连接 创建goroutine处理链接 示例代码:TCP服务端 package main import ( "net" &q ...
- go-zero 是如何实现令牌桶限流的?
原文链接: 上一篇文章介绍了 如何实现计数器限流?主要有两种实现方式,分别是固定窗口和滑动窗口,并且分析了 go-zero 采用固定窗口方式实现的源码. 但是采用固定窗口实现的限流器会有两个问题: 会 ...
- [碎碎念]和ljf老师聊天得到的一些启发,希望大家一起来吹水
关于写这个小文 和ljf老师聊天得到的一些启发,希望能够总结出来方便回顾,并且我觉得这些想法有一定的普适性,可以供大家参考. 疑问 我的疑问是,我现在主要在做fuzz+pwn,能够进行漏洞挖掘,以及w ...
- 小红书获得小红书笔记详情 API 返回值说明
item_get_video-获得小红书笔记详情 注册开通 smallredbook.item_get_video 公共参数 名称 类型 必须 描述 key String 是 调用key(必须以 ...
- 快速搭建SpringBoot3.x项目
写在前面 上一小节中我们从0到1 使用Vite搭建了一个Vue3项目,并集成了Element Plus 实现了一个简单的增删改查页面. 这一篇中我们将使用IDEA快速搭建一个SpringBoot3.x ...
- SK 简化流行编程语言对 生成式AI 应用开发的支持
Semantic Kernel[1] 是一个将大型语言模型(LLM)与流行的编程语言相结合的SDK. Microsoft将Semantic Kernel(简称SK)称为轻量级SDK,支持AI LLM的 ...
- 加密 K8s Secrets 的几种方案
前言 你可能已经听过很多遍这个不算秘密的秘密了--Kubernetes Secrets 不是加密的!Secret 的值是存储在 etcd 中的 base64 encoded(编码) 字符串.这意味着, ...
- 前端远程调试方案 Chii 的使用经验分享
前端远程调试方案 Chii 的使用经验分享 Chii 是与 weinre 一样的远程调试工具 ,主要是将 web inspector 替换为最新的 chrome devtools frontend 监 ...