v0.25.0
This commit is contained in:
1 parent
2e4aa4df4d
commit
87e571c170
106 files changed
+17952
-6185
No files matched your search
+270
-50
@@ -109,14 +109,31 @@ pub struct JsonFormat {
|
||||
pub static_fields: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
/// Capture a bounded User-Agent value in access logs and traffic analytics.
|
||||
pub capture_user_agent: bool,
|
||||
/// Resolve the effective client IP through the loaded GeoIP database.
|
||||
/// Disabled by default because this adds per-request lookup work.
|
||||
pub capture_country: bool,
|
||||
pub file: FileSink,
|
||||
pub logstash: LogstashSink,
|
||||
pub loki: LokiSink,
|
||||
pub format: JsonFormat,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
capture_user_agent: true,
|
||||
capture_country: false,
|
||||
file: FileSink::default(),
|
||||
logstash: LogstashSink::default(),
|
||||
loki: LokiSink::default(),
|
||||
format: JsonFormat::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.file.enabled || self.logstash.enabled || self.loki.enabled
|
||||
@@ -216,6 +233,9 @@ fn source_value_name(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"vhost"
|
||||
| "route"
|
||||
| "cache_effective_enabled"
|
||||
| "transform_effective_enabled"
|
||||
| "backend"
|
||||
| "id"
|
||||
| "time_ms"
|
||||
@@ -231,6 +251,8 @@ fn source_value_name(name: &str) -> bool {
|
||||
| "upstream_status"
|
||||
| "upstream_error"
|
||||
| "client_ip"
|
||||
| "user_agent"
|
||||
| "country"
|
||||
| "status"
|
||||
| "duration_ms"
|
||||
| "request_bytes"
|
||||
@@ -241,7 +263,6 @@ fn source_value_name(name: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum ErrorLogFormat {
|
||||
#[serde(rename = "JSON")]
|
||||
@@ -250,7 +271,9 @@ pub enum ErrorLogFormat {
|
||||
Raw,
|
||||
}
|
||||
impl Default for ErrorLogFormat {
|
||||
fn default() -> Self { Self::Json }
|
||||
fn default() -> Self {
|
||||
Self::Json
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -262,14 +285,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"
|
||||
);
|
||||
}
|
||||
@@ -319,7 +348,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));
|
||||
@@ -343,10 +379,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())
|
||||
@@ -357,7 +397,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)]
|
||||
@@ -368,21 +410,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;
|
||||
@@ -404,11 +467,18 @@ 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>,
|
||||
}
|
||||
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();
|
||||
@@ -418,10 +488,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 {
|
||||
@@ -462,17 +540,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) {
|
||||
@@ -487,7 +579,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);
|
||||
@@ -501,7 +596,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;
|
||||
@@ -526,7 +623,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))
|
||||
}
|
||||
|
||||
@@ -630,7 +729,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);
|
||||
@@ -667,6 +768,8 @@ pub struct LogEntry {
|
||||
pub upstream_status: Option<u16>,
|
||||
pub upstream_error: Option<String>,
|
||||
pub client_ip: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub status: u16,
|
||||
pub duration_ms: u64,
|
||||
pub request_bytes: u64,
|
||||
@@ -735,6 +838,12 @@ impl Logs {
|
||||
pub fn configure(&self, settings: Settings) {
|
||||
self.settings.store(Arc::new(settings));
|
||||
}
|
||||
pub fn capture_user_agent(&self) -> bool {
|
||||
self.settings.load().capture_user_agent
|
||||
}
|
||||
pub fn capture_country(&self) -> bool {
|
||||
self.settings.load().capture_country
|
||||
}
|
||||
pub fn push(&self, entry: LogEntry) {
|
||||
if let Ok(mut entries) = self.entries.try_lock() {
|
||||
if entries.len() == HISTORY_CAPACITY {
|
||||
@@ -758,7 +867,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 {
|
||||
@@ -778,6 +894,19 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"route" => entry
|
||||
.route
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"cache_effective_enabled" => entry
|
||||
.cache_effective_enabled
|
||||
.map(Value::Bool)
|
||||
.unwrap_or(Value::Null),
|
||||
"transform_effective_enabled" => entry
|
||||
.transform_effective_enabled
|
||||
.map(Value::Bool)
|
||||
.unwrap_or(Value::Null),
|
||||
"backend" => entry
|
||||
.backend
|
||||
.clone()
|
||||
@@ -790,13 +919,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),
|
||||
"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),
|
||||
@@ -855,12 +1017,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,
|
||||
@@ -902,15 +1076,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;
|
||||
@@ -931,8 +1113,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
|
||||
@@ -1062,6 +1252,8 @@ mod tests {
|
||||
upstream_status: Some(101),
|
||||
upstream_error: None,
|
||||
client_ip: Some("192.0.2.10".into()),
|
||||
user_agent: Some("RustProxy-Test/1.0".into()),
|
||||
country: Some("PL".into()),
|
||||
status: 200,
|
||||
duration_ms: 12,
|
||||
request_bytes: 10,
|
||||
@@ -1077,10 +1269,28 @@ mod tests {
|
||||
let mut format = JsonFormat::default();
|
||||
format.fields.insert("http_status".into(), "status".into());
|
||||
format.fields.insert("site".into(), "vhost".into());
|
||||
format.fields.insert("route".into(), "route".into());
|
||||
format.fields.insert("ua".into(), "user_agent".into());
|
||||
format.fields.insert("geo_country".into(), "country".into());
|
||||
format
|
||||
.fields
|
||||
.insert("cache_enabled".into(), "cache_effective_enabled".into());
|
||||
format.fields.insert(
|
||||
"transform_enabled".into(),
|
||||
"transform_effective_enabled".into(),
|
||||
);
|
||||
format.static_fields.insert("service".into(), "edge".into());
|
||||
let mut settings = Settings::default();
|
||||
settings.format = format.clone();
|
||||
settings.validate().unwrap();
|
||||
let value = format_entry(&entry(), &format);
|
||||
assert_eq!(value["http_status"], 200);
|
||||
assert_eq!(value["site"], "app");
|
||||
assert_eq!(value["route"], "root");
|
||||
assert_eq!(value["ua"], "RustProxy-Test/1.0");
|
||||
assert_eq!(value["geo_country"], "PL");
|
||||
assert_eq!(value["cache_enabled"], true);
|
||||
assert_eq!(value["transform_enabled"], false);
|
||||
assert_eq!(value["service"], "edge");
|
||||
assert!(value.get("method").is_none());
|
||||
}
|
||||
@@ -1117,6 +1327,13 @@ mod tests {
|
||||
assert!(settings.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_defaults_keep_geoip_off_request_path() {
|
||||
let settings = Settings::default();
|
||||
assert!(settings.capture_user_agent);
|
||||
assert!(!settings.capture_country);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_json_format_keeps_standard_entry() {
|
||||
let value = format_entry(&entry(), &JsonFormat::default());
|
||||
@@ -1167,6 +1384,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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user