move log-message formatting code to its own module
This commit is contained in:
@@ -3,10 +3,10 @@ use super::route::{Destination, Limit, Route};
|
|||||||
use super::{AdminMessage, LimitResult, Limiter, LimiterSet};
|
use super::{AdminMessage, LimitResult, Limiter, LimiterSet};
|
||||||
use crate::{
|
use crate::{
|
||||||
concurrent_map::ConcurrentMap,
|
concurrent_map::ConcurrentMap,
|
||||||
human_duration::HumanTMinus,
|
log_format::LogFormatConfig,
|
||||||
log_message::{LogMessage, Origin},
|
log_message::{LogMessage, Origin},
|
||||||
};
|
};
|
||||||
use chrono::{TimeDelta, Utc};
|
use chrono::Utc;
|
||||||
use conf::Conf;
|
use conf::Conf;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use tokio::sync::{Mutex, mpsc::UnboundedSender};
|
use tokio::sync::{Mutex, mpsc::UnboundedSender};
|
||||||
@@ -54,13 +54,12 @@ pub struct LogHandlerConfig {
|
|||||||
/// Overall rate limits applied after route checks pass.
|
/// Overall rate limits applied after route checks pass.
|
||||||
#[conf(long, env, value_parser = serde_json::from_str, default_value = "[]")]
|
#[conf(long, env, value_parser = serde_json::from_str, default_value = "[]")]
|
||||||
pub overall_limits: Vec<Limit>,
|
pub overall_limits: Vec<Limit>,
|
||||||
#[conf(long, env)]
|
|
||||||
pub format_module: bool,
|
|
||||||
#[conf(long, env)]
|
|
||||||
pub format_source_location: bool,
|
|
||||||
/// Number of recent log messages to buffer per origin
|
/// Number of recent log messages to buffer per origin
|
||||||
#[conf(long, env, default_value = "64")]
|
#[conf(long, env, default_value = "64")]
|
||||||
pub log_buffer_size: usize,
|
pub log_buffer_size: usize,
|
||||||
|
/// Log message formatting options.
|
||||||
|
#[conf(flatten)]
|
||||||
|
pub log_format: LogFormatConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The log handler takes log messages and decides what to do with them.
|
/// The log handler takes log messages and decides what to do with them.
|
||||||
@@ -132,7 +131,7 @@ impl LogHandler {
|
|||||||
buffer.with_iter(|iter| {
|
buffer.with_iter(|iter| {
|
||||||
writeln!(&mut text, "{} log messages (newest first):", iter.len()).unwrap();
|
writeln!(&mut text, "{} log messages (newest first):", iter.len()).unwrap();
|
||||||
for log_msg in iter {
|
for log_msg in iter {
|
||||||
self.write_log_msg(&mut text, log_msg, now);
|
self.config.log_format.write_log_msg(&mut text, log_msg, now);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
text.push('\n');
|
text.push('\n');
|
||||||
@@ -177,7 +176,7 @@ impl LogHandler {
|
|||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
|
|
||||||
buffer.push_back_and_drain(log_msg, |log_msg| {
|
buffer.push_back_and_drain(log_msg, |log_msg| {
|
||||||
self.write_log_msg(&mut text, log_msg, now);
|
self.config.log_format.write_log_msg(&mut text, log_msg, now);
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(text)
|
Some(text)
|
||||||
@@ -201,65 +200,6 @@ impl LogHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format a log message into a Writer, followed by \n, and using any config options to do so
|
|
||||||
fn write_log_msg(&self, mut writer: impl std::fmt::Write, log_msg: &LogMessage, now: i64) {
|
|
||||||
let sev = log_msg.level.to_str();
|
|
||||||
let msg = &log_msg.msg;
|
|
||||||
|
|
||||||
// Format relative timestamp if available
|
|
||||||
let time_str = if let Some(ts) = log_msg.timestamp {
|
|
||||||
let diff_secs = now.saturating_sub(ts);
|
|
||||||
HumanTMinus(TimeDelta::seconds(diff_secs)).to_string()
|
|
||||||
} else {
|
|
||||||
"T-?".to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract metadata from structured data if configured
|
|
||||||
let mut metadata_parts = Vec::new();
|
|
||||||
|
|
||||||
if self.config.format_module
|
|
||||||
&& let Some(module) = log_msg.module_path.as_ref()
|
|
||||||
{
|
|
||||||
metadata_parts.push(module.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.config.format_source_location {
|
|
||||||
let file_opt = log_msg.file.as_ref();
|
|
||||||
let line_opt = log_msg.line.as_ref();
|
|
||||||
|
|
||||||
let location = if let Some(file) = file_opt {
|
|
||||||
// Strip /home/{username}/ prefix if present
|
|
||||||
let trimmed_file = strip_prefix_and_one_slash(file, "/home/");
|
|
||||||
// Strip .cargo/registry/src/{hash}/ if present
|
|
||||||
let trimmed_file = strip_prefix_and_one_slash(trimmed_file, ".cargo/registry/src/");
|
|
||||||
|
|
||||||
if let Some(line) = line_opt {
|
|
||||||
format!("{trimmed_file}:{line}")
|
|
||||||
} else {
|
|
||||||
format!("{trimmed_file}:?")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No file present, use "?" even if line is present
|
|
||||||
"?".to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
metadata_parts.push(location);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format: "ERROR T-10s [foo bar.rs:42]: message"
|
|
||||||
// Pad severity to 5 chars (left-aligned), time to 8 chars (right-aligned)
|
|
||||||
let result = if metadata_parts.is_empty() {
|
|
||||||
writeln!(writer, "{:<5} {:>8}: {}", sev, time_str, msg)
|
|
||||||
} else {
|
|
||||||
let metadata = metadata_parts.join(" ");
|
|
||||||
writeln!(writer, "{:<5} {:>8} [{}]: {}", sev, time_str, metadata, msg)
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = result {
|
|
||||||
error!("Couldn't write log message ({err}): {sev}: {msg}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if a log message passes all rate limiters.
|
/// Check if a log message passes all rate limiters.
|
||||||
///
|
///
|
||||||
/// Tests the message against each route's filter in succession (no early return).
|
/// Tests the message against each route's filter in succession (no early return).
|
||||||
@@ -326,16 +266,3 @@ impl LogHandler {
|
|||||||
Ok(first_destination)
|
Ok(first_destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip a prefix, then find the first remaining slash and skip up to that as well.
|
|
||||||
fn strip_prefix_and_one_slash<'a>(target: &'a str, prefix: &str) -> &'a str {
|
|
||||||
let Some(target) = target.strip_prefix(prefix) else {
|
|
||||||
return target;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((_, after)) = target.split_once('/') {
|
|
||||||
after
|
|
||||||
} else {
|
|
||||||
target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ pub mod message_handler;
|
|||||||
pub(crate) mod circular_buffer;
|
pub(crate) mod circular_buffer;
|
||||||
pub(crate) mod concurrent_map;
|
pub(crate) mod concurrent_map;
|
||||||
pub(crate) mod human_duration;
|
pub(crate) mod human_duration;
|
||||||
pub(crate) mod signal_jsonrpc;
|
pub(crate) mod log_format;
|
||||||
pub(crate) mod log_message;
|
pub(crate) mod log_message;
|
||||||
|
pub(crate) mod signal_jsonrpc;
|
||||||
pub(crate) mod prometheus;
|
pub(crate) mod prometheus;
|
||||||
pub(crate) mod transports;
|
pub(crate) mod transports;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
//! Log message formatting configuration.
|
||||||
|
//!
|
||||||
|
//! This module provides configuration and formatting for log messages,
|
||||||
|
//! controlling how they are rendered for alerts.
|
||||||
|
|
||||||
|
use crate::human_duration::HumanTMinus;
|
||||||
|
use crate::log_message::LogMessage;
|
||||||
|
use chrono::TimeDelta;
|
||||||
|
use conf::Conf;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
/// Configuration for log message formatting.
|
||||||
|
#[derive(Clone, Conf, Debug, Default)]
|
||||||
|
pub struct LogFormatConfig {
|
||||||
|
/// Include the module path in formatted output.
|
||||||
|
#[conf(long, env)]
|
||||||
|
pub format_module: bool,
|
||||||
|
/// Include the source file and line number in formatted output.
|
||||||
|
#[conf(long, env)]
|
||||||
|
pub format_source_location: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogFormatConfig {
|
||||||
|
/// Format a log message into a Writer, followed by \n.
|
||||||
|
///
|
||||||
|
/// The `now` parameter is the current timestamp in seconds, used to
|
||||||
|
/// calculate relative timestamps (e.g., "T-10s").
|
||||||
|
pub fn write_log_msg(&self, mut writer: impl std::fmt::Write, log_msg: &LogMessage, now: i64) {
|
||||||
|
let sev = log_msg.level.to_str();
|
||||||
|
let msg = &log_msg.msg;
|
||||||
|
|
||||||
|
// Format relative timestamp if available
|
||||||
|
let time_str = if let Some(ts) = log_msg.timestamp {
|
||||||
|
let diff_secs = now.saturating_sub(ts);
|
||||||
|
HumanTMinus(TimeDelta::seconds(diff_secs)).to_string()
|
||||||
|
} else {
|
||||||
|
"T-?".to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract metadata from structured data if configured
|
||||||
|
let mut metadata_parts = Vec::new();
|
||||||
|
|
||||||
|
if self.format_module
|
||||||
|
&& let Some(module) = log_msg.module_path.as_ref()
|
||||||
|
{
|
||||||
|
metadata_parts.push(module.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.format_source_location {
|
||||||
|
let file_opt = log_msg.file.as_ref();
|
||||||
|
let line_opt = log_msg.line.as_ref();
|
||||||
|
|
||||||
|
let location = if let Some(file) = file_opt {
|
||||||
|
// Strip /home/{username}/ prefix if present
|
||||||
|
let trimmed_file = strip_prefix_and_one_slash(file, "/home/");
|
||||||
|
// Strip .cargo/registry/src/{hash}/ if present
|
||||||
|
let trimmed_file = strip_prefix_and_one_slash(trimmed_file, ".cargo/registry/src/");
|
||||||
|
|
||||||
|
if let Some(line) = line_opt {
|
||||||
|
format!("{trimmed_file}:{line}")
|
||||||
|
} else {
|
||||||
|
format!("{trimmed_file}:?")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No file present, use "?" even if line is present
|
||||||
|
"?".to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
metadata_parts.push(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format: "ERROR T-10s [foo bar.rs:42]: message"
|
||||||
|
// Pad severity to 5 chars (left-aligned), time to 8 chars (right-aligned)
|
||||||
|
let result = if metadata_parts.is_empty() {
|
||||||
|
writeln!(writer, "{:<5} {:>8}: {}", sev, time_str, msg)
|
||||||
|
} else {
|
||||||
|
let metadata = metadata_parts.join(" ");
|
||||||
|
writeln!(writer, "{:<5} {:>8} [{}]: {}", sev, time_str, metadata, msg)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Couldn't write log message ({err}): {sev}: {msg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip a prefix, then find the first remaining slash and skip up to that as well.
|
||||||
|
fn strip_prefix_and_one_slash<'a>(target: &'a str, prefix: &str) -> &'a str {
|
||||||
|
let Some(target) = target.strip_prefix(prefix) else {
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((_, after)) = target.split_once('/') {
|
||||||
|
after
|
||||||
|
} else {
|
||||||
|
target
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user