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"

日志的组成部分

日志的组成分为三个部分

  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做键值对,防止重复:

/// 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}做示范,我们在解析这字符串的时候将会得到以下五个部分:

  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

小结

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

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

28. 干货系列从零用Rust编写正反向代理,项目日志的源码实现的更多相关文章

  1. arcgis api 3.x for js 共享干货系列之二自定义 Navigation 控件样式风格(附源码下载)

    0.内容概览 自定义 Navigation 控件样式风格 源码下载 1.内容讲解 arcgis api 3.x for js 默认的Navigation控件样式风格如下图:这样的风格不能说不好,各有各 ...

  2. PHP扩展编写、PHP扩展调试、VLD源码分析、基于嵌入式Embed SAPI实现opcode查看

    catalogue . 编译PHP源码 . 扩展结构.优缺点 . 使用PHP原生扩展框架wizard ext_skel编写扩展 . 编译安装VLD . Debug调试VLD . VLD源码分析 . 嵌 ...

  3. 《手把手教你》系列技巧篇(六)-java+ selenium自动化测试-阅读selenium源码(详细教程)

    1.简介 前面几篇基础系列文章,足够你迈进了Selenium门槛,再不济你也至少知道如何写你第一个基于Java的Selenium自动化测试脚本.接下来宏哥介绍Selenium技巧篇,主要是介绍一些常用 ...

  4. MVVM架构~knockoutjs系列之表单添加(验证)与列表操作源码开放

    返回目录 本文章应该是knockoutjs系列的最后一篇了,前几篇中主要讲一些基础知识,这一讲主要是一个实际的例子,对于一个对象的添加与编辑功能的实现,并将项目源代码公开了,共大家一起学习! knoc ...

  5. arcgis api 3.x for js 入门开发系列批量叠加 zip 压缩 SHP 图层优化篇(附源码下载)

    前言 关于本篇功能实现用到的 api 涉及类看不懂的,请参照 esri 官网的 arcgis api 3.x for js:esri 官网 api,里面详细的介绍 arcgis api 3.x 各个类 ...

  6. Springboot系列:Springboot与Thymeleaf模板引擎整合基础教程(附源码)

    前言 由于在开发My Blog项目时使用了大量的技术整合,针对于部分框架的使用和整合的流程没有做详细的介绍和记录,导致有些朋友用起来有些吃力,因此打算在接下来的时间里做一些基础整合的介绍,当然,可能也 ...

  7. vue系列---理解Vue中的computed,watch,methods的区别及源码实现(六)

    _ 阅读目录 一. 理解Vue中的computed用法 二:computed 和 methods的区别? 三:Vue中的watch的用法 四:computed的基本原理及源码实现 回到顶部 一. 理解 ...

  8. leaflet-webpack 入门开发系列二加载不同在线地图切换显示(附源码下载)

    前言 leaflet-webpack 入门开发系列环境知识点了解: node 安装包下载webpack 打包管理工具需要依赖 node 环境,所以 node 安装包必须安装,上面链接是官网下载地址 w ...

  9. 深入浅出Mybatis系列三-配置详解之properties与environments(mybatis源码篇)

    注:本文转载自南轲梦 注:博主 Chloneda:个人博客 | 博客园 | Github | Gitee | 知乎 上篇文章<深入浅出Mybatis系列(二)---配置简介(mybatis源码篇 ...

  10. openlayers4 入门开发系列之批量叠加 zip 压缩 SHP 图层篇(附源码下载)

    前言 openlayers4 官网的 api 文档介绍地址 openlayers4 api,里面详细的介绍 openlayers4 各个类的介绍,还有就是在线例子:openlayers4 官网在线例子 ...

随机推荐

  1. BugKu:文件包含+php伪协议

    这道题一进去发现一个超连接点击后发现跳转到了如下页面url如下/index.php?file=show.php,觉得这道题应该是一个php伪协议的应用 1 php://filter php://fil ...

  2. 什么是PMP?

    PMP(Project Management Professional)中文名称叫项目管理专业人士资格认证.它是由美国项目管理协会(PMI)发起的,严格评估项目管理人员知识技能是否具有高品质的资格认证 ...

  3. 新一代开源流数据湖平台Apache Paimon入门实操-上

    @ 目录 概述 定义 核心功能 适用场景 架构原理 总体架构 统一存储 基本概念 文件布局 部署 环境准备 环境部署 实战 Catalog 文件系统 Hive Catalog 创建表 创建Catalo ...

  4. 《SQL与数据库基础》18. MySQL管理

    目录 MySQL管理 系统数据库 常用工具 mysql mysqladmin mysqlbinlog mysqlshow mysqldump mysqlimport source 本文以 MySQL ...

  5. Codeforces-1095E-Almost-Regular-Bracket-Sequence

    题意 给定一个长度为 \(n\) 的小括号序列,求有多少个位置满足将这个位置的括号方向反过来后使得新序列是一个合法的括号序列.即在任意一个位置前缀左括号的个数不少于前缀右括号的个数,同时整个序列左右括 ...

  6. 解决SVN死锁问题

    svn执行clean up后出现提示:svn cleanup failed–previous operation has not finished; run cleanup if it was int ...

  7. defined('BASEPATH') OR exit('No direct script access allowed'); 的作用

    起到保护.php文件的作用, 如果直接访问此php文件会得到"不允许直接访问脚本"的错误提示 如果你是用ci框架或者其他的什么, 就建议加上, 如果你怕别人恶意攻击你的话

  8. UM 百度富文本编辑器自定义图片上传路径

    UM 百度富文本编辑器自定义图片上传路径 因为公司要做图文编辑,选择了UM,但是直接存入Tomcat根目录下,不满足业务需求需要存入服务器上. 一.需要注意的是在um的JSP目录下已经存在了Uploa ...

  9. nginx Ingress Controller Packaged by Bitnami

    环境介绍 节点 master01 work01 work02 主机/ip calico-master01/192.168.195.135 calico-master01/192.168.195.135 ...

  10. 5 分钟理解 Next.js SSG (Static Site Generation / Static Export)

    5 分钟理解 Next.js SSG (Static Site Generation / Static Export) 在本篇文章中,我们将介绍 Next.js 中的 SSG(静态网站生成)功能,以及 ...