This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 09:30:21 +02:00
1 parent 1dea7f55c8
commit c819f0a335
151 files changed
+7610 -4264

No files matched your search

+98 -24
View File
@@ -1,7 +1,12 @@
impl IoWrite for RedactingWriter {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> { self.buffer.extend_from_slice(bytes); Ok(bytes.len()) }
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.buffer.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
if self.buffer.is_empty() { return Ok(()); }
if self.buffer.is_empty() {
return Ok(());
}
let text = String::from_utf8_lossy(&self.buffer);
let redacted = redact_text(&text);
let mut stderr = io::stderr().lock();
@@ -11,10 +16,18 @@ impl IoWrite for RedactingWriter {
Ok(())
}
}
impl Drop for RedactingWriter { fn drop(&mut self) { let _ = self.flush(); } }
impl Drop for RedactingWriter {
fn drop(&mut self) {
let _ = self.flush();
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for RedactingStderr {
type Writer = RedactingWriter;
fn make_writer(&'a self) -> Self::Writer { RedactingWriter { buffer: Vec::with_capacity(512) } }
fn make_writer(&'a self) -> Self::Writer {
RedactingWriter {
buffer: Vec::with_capacity(512),
}
}
}
fn redact_regex(pattern: &'static str) -> &'static regex::Regex {
@@ -55,17 +68,31 @@ fn is_secret_field(name: &str) -> bool {
}
pub fn redact_text(input: &str) -> String {
let mut value = redact_regex("pem").replace_all(input, "[REDACTED PRIVATE KEY]").into_owned();
value = redact_regex("auth").replace_all(&value, "$1[REDACTED]").into_owned();
value = redact_regex("query").replace_all(&value, "$1[REDACTED]").into_owned();
value = redact_regex("json").replace_all(&value, "$1[REDACTED]$2").into_owned();
redact_regex("kv").replace_all(&value, "$1[REDACTED]").into_owned()
let mut value = redact_regex("pem")
.replace_all(input, "[REDACTED PRIVATE KEY]")
.into_owned();
value = redact_regex("auth")
.replace_all(&value, "$1[REDACTED]")
.into_owned();
value = redact_regex("query")
.replace_all(&value, "$1[REDACTED]")
.into_owned();
value = redact_regex("json")
.replace_all(&value, "$1[REDACTED]$2")
.into_owned();
redact_regex("kv")
.replace_all(&value, "$1[REDACTED]")
.into_owned()
}
fn redact_value(value: &mut Value) {
match value {
Value::String(text) => *text = redact_text(text),
Value::Array(items) => for item in items { redact_value(item); },
Value::Array(items) => {
for item in items {
redact_value(item);
}
}
Value::Object(items) => {
for (key, item) in items {
if is_secret_field(key) {
@@ -80,7 +107,10 @@ fn redact_value(value: &mut Value) {
}
fn rfc3339_now() -> String {
let ms = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as i64;
let ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
let seconds = ms / 1000;
let millis = ms % 1000;
let days = seconds.div_euclid(86_400);
@@ -94,7 +124,9 @@ fn rfc3339_now() -> String {
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = mp + if mp < 10 { 3 } else { -9 };
if month <= 2 { year += 1; }
if month <= 2 {
year += 1;
}
let hour = day_seconds / 3600;
let minute = (day_seconds % 3600) / 60;
let second = day_seconds % 60;
@@ -119,7 +151,9 @@ async fn open_error_file(path: PathBuf) -> Result<File> {
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
}
Ok(file)
}).await.map_err(|_| anyhow::anyhow!("error log open worker failed"))??;
})
.await
.map_err(|_| anyhow::anyhow!("error log open worker failed"))??;
Ok(File::from_std(file))
}
@@ -223,7 +257,9 @@ async fn error_log_loop(
}
}
}
let Some(opened) = file.as_mut() else { continue; };
let Some(opened) = file.as_mut() else {
continue;
};
let line = format_error_event(&event, config.format);
if let Err(error) = opened.write_all(&line).await {
errors.fetch_add(1, Ordering::Relaxed);
@@ -360,7 +396,14 @@ impl Logs {
pub fn recent_limit(&self, limit: usize) -> Vec<LogEntry> {
self.entries
.lock()
.map(|entries| entries.iter().rev().take(limit.min(HISTORY_CAPACITY)).cloned().collect())
.map(|entries| {
entries
.iter()
.rev()
.take(limit.min(HISTORY_CAPACITY))
.cloned()
.collect()
})
.unwrap_or_default()
}
pub fn stats(&self) -> SinkStats {
@@ -406,15 +449,46 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
"path" => json!(&entry.path),
"http_version" => json!(entry.http_version),
"transport" => json!(entry.transport),
"websocket_bootstrap" => entry.websocket_bootstrap.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
"websocket_phase" => entry.websocket_phase.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
"websocket_error" => entry.websocket_error.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
"h3_reset_code" => entry.h3_reset_code.map(|v| Value::String(v.into())).unwrap_or(Value::Null),
"upstream_status" => entry.upstream_status.map(|v| json!(v)).unwrap_or(Value::Null),
"upstream_error" => entry.upstream_error.clone().map(Value::String).unwrap_or(Value::Null),
"client_ip" => entry.client_ip.clone().map(Value::String).unwrap_or(Value::Null),
"user_agent" => entry.user_agent.clone().map(Value::String).unwrap_or(Value::Null),
"country" => entry.country.clone().map(Value::String).unwrap_or(Value::Null),
"websocket_bootstrap" => entry
.websocket_bootstrap
.map(|v| Value::String(v.into()))
.unwrap_or(Value::Null),
"websocket_phase" => entry
.websocket_phase
.map(|v| Value::String(v.into()))
.unwrap_or(Value::Null),
"websocket_error" => entry
.websocket_error
.map(|v| Value::String(v.into()))
.unwrap_or(Value::Null),
"h3_reset_code" => entry
.h3_reset_code
.map(|v| Value::String(v.into()))
.unwrap_or(Value::Null),
"upstream_status" => entry
.upstream_status
.map(|v| json!(v))
.unwrap_or(Value::Null),
"upstream_error" => entry
.upstream_error
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
"client_ip" => entry
.client_ip
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
"user_agent" => entry
.user_agent
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
"country" => entry
.country
.clone()
.map(Value::String)
.unwrap_or(Value::Null),
"status" => json!(entry.status),
"duration_ms" => json!(entry.duration_ms),
"request_bytes" => json!(entry.request_bytes),
+61 -18
View File
@@ -261,7 +261,6 @@ fn source_value_name(name: &str) -> bool {
)
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ErrorLogFormat {
#[serde(rename = "JSON")]
@@ -270,7 +269,9 @@ pub enum ErrorLogFormat {
Raw,
}
impl Default for ErrorLogFormat {
fn default() -> Self { Self::Json }
fn default() -> Self {
Self::Json
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -282,14 +283,20 @@ pub struct ErrorLogSettings {
}
impl Default for ErrorLogSettings {
fn default() -> Self {
Self { enabled: false, path: "/var/log/rustproxy/error.log".into(), format: ErrorLogFormat::Json }
Self {
enabled: false,
path: "/var/log/rustproxy/error.log".into(),
format: ErrorLogFormat::Json,
}
}
}
impl ErrorLogSettings {
pub fn validate(&self) -> Result<()> {
if self.enabled {
ensure!(
!self.path.trim().is_empty() && self.path.len() <= 1024 && !self.path.chars().any(char::is_control),
!self.path.trim().is_empty()
&& self.path.len() <= 1024
&& !self.path.chars().any(char::is_control),
"error_log.path must be 1..1024 bytes without controls"
);
}
@@ -339,7 +346,14 @@ impl ErrorLogController {
written.clone(),
errors.clone(),
));
Arc::new(Self { settings, settings_changed, sender, dropped, written, errors })
Arc::new(Self {
settings,
settings_changed,
sender,
dropped,
written,
errors,
})
}
pub fn configure(&self, settings: ErrorLogSettings) {
self.settings.store(Arc::new(settings));
@@ -363,10 +377,14 @@ impl ErrorLogController {
static ERROR_LOG: OnceLock<Arc<ErrorLogController>> = OnceLock::new();
pub fn install_error_log_controller(controller: Arc<ErrorLogController>) -> Result<()> {
ERROR_LOG.set(controller).map_err(|_| anyhow::anyhow!("error logger already initialized"))
ERROR_LOG
.set(controller)
.map_err(|_| anyhow::anyhow!("error logger already initialized"))
}
pub fn configure_error_log(settings: ErrorLogSettings) {
if let Some(controller) = ERROR_LOG.get() { controller.configure(settings); }
if let Some(controller) = ERROR_LOG.get() {
controller.configure(settings);
}
}
pub fn error_log_stats() -> Option<ErrorLogStats> {
ERROR_LOG.get().map(|controller| controller.stats())
@@ -377,7 +395,9 @@ pub struct ErrorLogLayer {
controller: Arc<ErrorLogController>,
}
impl ErrorLogLayer {
pub fn new(controller: Arc<ErrorLogController>) -> Self { Self { controller } }
pub fn new(controller: Arc<ErrorLogController>) -> Self {
Self { controller }
}
}
#[derive(Default)]
@@ -388,21 +408,42 @@ struct EventVisitor {
impl tracing::field::Visit for EventVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
let value = format!("{value:?}");
if field.name() == "message" { self.message = Some(value); }
else { self.fields.insert(field.name().to_owned(), value); }
if field.name() == "message" {
self.message = Some(value);
} else {
self.fields.insert(field.name().to_owned(), value);
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" { self.message = Some(value.to_owned()); }
else { self.fields.insert(field.name().to_owned(), value.to_owned()); }
if field.name() == "message" {
self.message = Some(value.to_owned());
} else {
self.fields
.insert(field.name().to_owned(), value.to_owned());
}
}
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
self.fields
.insert(field.name().to_owned(), value.to_string());
}
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
self.fields
.insert(field.name().to_owned(), value.to_string());
}
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
self.fields
.insert(field.name().to_owned(), value.to_string());
}
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
self.fields
.insert(field.name().to_owned(), value.to_string());
}
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { self.fields.insert(field.name().to_owned(), value.to_string()); }
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { self.fields.insert(field.name().to_owned(), value.to_string()); }
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { self.fields.insert(field.name().to_owned(), value.to_string()); }
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { self.fields.insert(field.name().to_owned(), value.to_string()); }
}
impl<S: Subscriber> Layer<S> for ErrorLogLayer {
fn on_event(&self, event: &Event<'_>, _ctx: LayerContext<'_, S>) {
if *event.metadata().level() != Level::ERROR { return; }
if *event.metadata().level() != Level::ERROR {
return;
}
let mut visitor = EventVisitor::default();
event.record(&mut visitor);
let mut fields = visitor.fields;
@@ -424,4 +465,6 @@ impl<S: Subscriber> Layer<S> for ErrorLogLayer {
#[derive(Clone, Copy, Default)]
pub struct RedactingStderr;
pub struct RedactingWriter { buffer: Vec<u8> }
pub struct RedactingWriter {
buffer: Vec<u8>,
}
+36 -8
View File
@@ -46,12 +46,24 @@ struct FileState {
const PER_VHOST_OPEN_FILES: usize = 64;
fn vhost_log_name(vhost: Option<&str>) -> String {
let raw = vhost.filter(|value| !value.is_empty()).unwrap_or("_unmatched");
let raw = vhost
.filter(|value| !value.is_empty())
.unwrap_or("_unmatched");
let mut safe = String::with_capacity(raw.len().min(96));
for ch in raw.chars().take(96) {
safe.push(if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') { ch } else { '_' });
safe.push(
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
ch
} else {
'_'
},
);
}
if safe.is_empty() {
"_unmatched".into()
} else {
safe
}
if safe.is_empty() { "_unmatched".into() } else { safe }
}
struct LogstashState {
address: String,
@@ -93,15 +105,23 @@ async fn sink_loop(
if tokio::fs::create_dir_all(&directory).await.is_err() {
failed = true;
} else {
let path = directory.join(format!("{}.jsonl", vhost_log_name(entry.vhost.as_deref())));
let path =
directory.join(format!("{}.jsonl", vhost_log_name(entry.vhost.as_deref())));
if !vhost_files.contains_key(&path) {
if vhost_files.len() >= PER_VHOST_OPEN_FILES {
if let Some(path) = vhost_files.keys().next().cloned() {
vhost_files.remove(&path);
}
}
match OpenOptions::new().create(true).append(true).open(&path).await {
Ok(opened) => { vhost_files.insert(path.clone(), opened); }
match OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await
{
Ok(opened) => {
vhost_files.insert(path.clone(), opened);
}
Err(error) => {
tracing::warn!(%error, path=%path.display(), "per-vhost access log file unavailable");
failed = true;
@@ -122,8 +142,16 @@ async fn sink_loop(
vhost_directory = None;
let path = PathBuf::from(&config.file.path);
if file.as_ref().is_none_or(|state| state.path != path) {
file = match OpenOptions::new().create(true).append(true).open(&path).await {
Ok(opened) => Some(FileState { path: path.clone(), file: opened }),
file = match OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.await
{
Ok(opened) => Some(FileState {
path: path.clone(),
file: opened,
}),
Err(error) => {
tracing::warn!(%error, path=%path.display(), "access log file unavailable");
None
+4 -1
View File
@@ -155,6 +155,9 @@ mod tests {
}
assert_eq!(logs.recent_limit(LIVE_WINDOW).len(), LIVE_WINDOW);
assert_eq!(logs.recent().len(), LIVE_WINDOW + 80);
assert_eq!(logs.recent_limit(HISTORY_CAPACITY + 1).len(), LIVE_WINDOW + 80);
assert_eq!(
logs.recent_limit(HISTORY_CAPACITY + 1).len(),
LIVE_WINDOW + 80
);
}
}