Merge remote-tracking branch 'origin/feat/ops/logging'
This commit is contained in:
79
Cargo.lock
generated
79
Cargo.lock
generated
@@ -17,6 +17,15 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1136,6 +1145,15 @@ version = "0.4.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata 0.1.10",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@@ -1474,6 +1492,50 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata 0.4.14",
|
||||||
|
"regex-syntax 0.8.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||||
|
dependencies = [
|
||||||
|
"regex-syntax 0.6.29",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax 0.8.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.6.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.12.28"
|
version = "0.12.28"
|
||||||
@@ -2213,18 +2275,35 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-serde"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-subscriber"
|
name = "tracing-subscriber"
|
||||||
version = "0.3.19"
|
version = "0.3.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
"nu-ansi-term",
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sharded-slab",
|
"sharded-slab",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thread_local",
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
|
"tracing-serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ serde = { version = "=1.0.228", features = ["derive"] }
|
|||||||
toml = "=0.8.23"
|
toml = "=0.8.23"
|
||||||
arc-swap = "=1.7.1"
|
arc-swap = "=1.7.1"
|
||||||
tracing = "=0.1.41"
|
tracing = "=0.1.41"
|
||||||
tracing-subscriber = "=0.3.19"
|
tracing-subscriber = { version = "=0.3.19", features = ["env-filter", "json"] }
|
||||||
rustls-pemfile = "=2.2.0"
|
rustls-pemfile = "=2.2.0"
|
||||||
rustls-pki-types = "=1.12.0"
|
rustls-pki-types = "=1.12.0"
|
||||||
clap = { version = "=4.6.1", features = ["derive"] }
|
clap = { version = "=4.6.1", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,2 +1,187 @@
|
|||||||
#[allow(dead_code)]
|
#[derive(Default)]
|
||||||
pub struct LogFormat;
|
struct KvVisitor {
|
||||||
|
pairs: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KvVisitor {
|
||||||
|
fn format(&self) -> String {
|
||||||
|
let parts: Vec<String> = self
|
||||||
|
.pairs
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| {
|
||||||
|
if v.contains(' ') || v.contains('"') {
|
||||||
|
format!(r#"{}="{}""#, k, v.replace('"', "\\\""))
|
||||||
|
} else {
|
||||||
|
format!("{}={}", k, v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
parts.join(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl tracing::field::Visit for KvVisitor {
|
||||||
|
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
|
||||||
|
self.pairs
|
||||||
|
.push((field.name().to_string(), value.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
|
||||||
|
self.pairs
|
||||||
|
.push((field.name().to_string(), format!("{:?}", value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
|
||||||
|
self.pairs
|
||||||
|
.push((field.name().to_string(), value.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
|
||||||
|
self.pairs
|
||||||
|
.push((field.name().to_string(), value.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
|
||||||
|
self.pairs
|
||||||
|
.push((field.name().to_string(), value.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_event_fields(event: &tracing::Event<'_>) -> String {
|
||||||
|
let mut visitor = KvVisitor::default();
|
||||||
|
event.record(&mut visitor);
|
||||||
|
visitor.format()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_request {
|
||||||
|
($client_ip:expr, $host:expr, $method:expr, $path:expr, $status:expr, $upstream:expr, $duration_ms:expr) => {
|
||||||
|
tracing::info!(
|
||||||
|
prefix = "REQUEST",
|
||||||
|
client_ip = %$client_ip,
|
||||||
|
host = %$host,
|
||||||
|
method = %$method,
|
||||||
|
path = %$path,
|
||||||
|
status = %$status,
|
||||||
|
upstream = %$upstream,
|
||||||
|
duration_ms = %$duration_ms,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_rate_limit {
|
||||||
|
($client_ip:expr, $host:expr, $path:expr, $status:expr) => {
|
||||||
|
tracing::warn!(
|
||||||
|
prefix = "RATE_LIMIT",
|
||||||
|
client_ip = %$client_ip,
|
||||||
|
host = %$host,
|
||||||
|
path = %$path,
|
||||||
|
status = %$status,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_upstream_error {
|
||||||
|
($host:expr, $upstream:expr, $error:expr) => {
|
||||||
|
tracing::warn!(
|
||||||
|
prefix = "UPSTREAM_ERROR",
|
||||||
|
host = %$host,
|
||||||
|
upstream = %$upstream,
|
||||||
|
error = %$error,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_config_reload {
|
||||||
|
($status:expr, $sites:expr) => {
|
||||||
|
tracing::info!(
|
||||||
|
prefix = "CONFIG_RELOAD",
|
||||||
|
status = %$status,
|
||||||
|
sites = %$sites,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_visitor_formats_simple_key_value() {
|
||||||
|
let mut visitor = KvVisitor::default();
|
||||||
|
visitor
|
||||||
|
.pairs
|
||||||
|
.push(("client_ip".to_string(), "203.0.113.50".to_string()));
|
||||||
|
visitor
|
||||||
|
.pairs
|
||||||
|
.push(("status".to_string(), "200".to_string()));
|
||||||
|
assert_eq!(visitor.format(), "client_ip=203.0.113.50 status=200");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_visitor_quotes_values_with_spaces() {
|
||||||
|
let mut visitor = KvVisitor::default();
|
||||||
|
visitor
|
||||||
|
.pairs
|
||||||
|
.push(("error".to_string(), "connection refused".to_string()));
|
||||||
|
assert_eq!(visitor.format(), r#"error="connection refused""#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_visitor_escapes_quotes_in_values() {
|
||||||
|
let mut visitor = KvVisitor::default();
|
||||||
|
visitor
|
||||||
|
.pairs
|
||||||
|
.push(("error".to_string(), r#"connection "timeout""#.to_string()));
|
||||||
|
let formatted = visitor.format();
|
||||||
|
assert!(
|
||||||
|
formatted.contains(r#"connection \"timeout\""#),
|
||||||
|
"got: {}",
|
||||||
|
formatted
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_visitor_handles_numeric_values() {
|
||||||
|
let mut visitor = KvVisitor::default();
|
||||||
|
visitor
|
||||||
|
.pairs
|
||||||
|
.push(("status".to_string(), "200".to_string()));
|
||||||
|
visitor
|
||||||
|
.pairs
|
||||||
|
.push(("duration_ms".to_string(), "45".to_string()));
|
||||||
|
let formatted = visitor.format();
|
||||||
|
assert!(formatted.contains("status=200"));
|
||||||
|
assert!(formatted.contains("duration_ms=45"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn kv_visitor_handles_empty() {
|
||||||
|
let visitor = KvVisitor::default();
|
||||||
|
assert_eq!(visitor.format(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn log_macros_compile() {
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
|
||||||
|
let subscriber = tracing_subscriber::registry().with(tracing_subscriber::fmt::layer());
|
||||||
|
let _guard = tracing::subscriber::set_default(subscriber);
|
||||||
|
|
||||||
|
log_request!(
|
||||||
|
"203.0.113.50",
|
||||||
|
"git.alk.dev",
|
||||||
|
"GET",
|
||||||
|
"/user/repo",
|
||||||
|
200,
|
||||||
|
"127.0.0.1:3000",
|
||||||
|
45u64
|
||||||
|
);
|
||||||
|
log_rate_limit!("10.0.0.1", "example.com", "/login", 429u16);
|
||||||
|
log_upstream_error!("git.alk.dev", "127.0.0.1:3000", "connection refused");
|
||||||
|
log_config_reload!("success", 1u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1,112 @@
|
|||||||
pub mod format;
|
pub mod format;
|
||||||
|
|
||||||
|
use crate::config::static_config::LoggingConfig;
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::Level;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use tracing_subscriber::Layer;
|
||||||
|
|
||||||
|
pub fn init(config: &LoggingConfig) -> Result<()> {
|
||||||
|
let level = config.level.parse::<Level>().unwrap_or(Level::INFO);
|
||||||
|
|
||||||
|
let env_filter = make_env_filter(level);
|
||||||
|
|
||||||
|
match config.format.as_str() {
|
||||||
|
"json" => init_json(env_filter, &config.log_file_path, level),
|
||||||
|
_ => init_text(env_filter, &config.log_file_path, level),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_env_filter(level: Level) -> EnvFilter {
|
||||||
|
EnvFilter::from_default_env().add_directive(level.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_json(env_filter: EnvFilter, log_file_path: &Option<String>, level: Level) -> Result<()> {
|
||||||
|
match log_file_path {
|
||||||
|
Some(path) => {
|
||||||
|
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||||
|
if !parent.as_os_str().is_empty() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let file = File::create(path)?;
|
||||||
|
let file_writer = Arc::new(file);
|
||||||
|
|
||||||
|
let file_env_filter = make_env_filter(level);
|
||||||
|
let stdout_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.json()
|
||||||
|
.with_filter(env_filter);
|
||||||
|
let file_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.json()
|
||||||
|
.with_writer(file_writer)
|
||||||
|
.with_filter(file_env_filter);
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(stdout_layer)
|
||||||
|
.with(file_layer)
|
||||||
|
.try_init()?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let layer = tracing_subscriber::fmt::layer()
|
||||||
|
.json()
|
||||||
|
.with_filter(env_filter);
|
||||||
|
tracing_subscriber::registry().with(layer).try_init()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_text(env_filter: EnvFilter, log_file_path: &Option<String>, level: Level) -> Result<()> {
|
||||||
|
match log_file_path {
|
||||||
|
Some(path) => {
|
||||||
|
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||||
|
if !parent.as_os_str().is_empty() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let file = File::create(path)?;
|
||||||
|
let file_writer = Arc::new(file);
|
||||||
|
|
||||||
|
let file_env_filter = make_env_filter(level);
|
||||||
|
let stdout_layer = tracing_subscriber::fmt::layer().with_filter(env_filter);
|
||||||
|
let file_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_writer(file_writer)
|
||||||
|
.with_filter(file_env_filter);
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(stdout_layer)
|
||||||
|
.with(file_layer)
|
||||||
|
.try_init()?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let layer = tracing_subscriber::fmt::layer().with_filter(env_filter);
|
||||||
|
tracing_subscriber::registry().with(layer).try_init()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config::static_config::LoggingConfig;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_creates_log_directory_and_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let log_path = dir.path().join("logs").join("access.log");
|
||||||
|
let config = LoggingConfig {
|
||||||
|
level: "info".to_string(),
|
||||||
|
format: "text".to_string(),
|
||||||
|
log_file_path: Some(log_path.to_string_lossy().to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = init(&config);
|
||||||
|
assert!(result.is_ok(), "init failed: {:?}", result.err());
|
||||||
|
assert!(log_path.exists(), "log file should be created");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user