wmproxy wmproxy已用Rust實現http/https代理, socks5代理, 反向代理, 靜態文件伺服器,四層TCP/UDP轉發,內網穿透,後續將實現websocket代理等,會將實現過程分享出來,感興趣的可以一起造個輪子 項目地址 國內: https://gitee.com/tic ...
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:00
url:
常量,原樣輸出{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
小結
日誌在程式中必不可少,那麼需要儘可能的高效,所以儘可能的提升日誌的效率是必須處理的一環。
點擊 [關註],[在看],[點贊] 是對作者最大的支持