v0.34.0_9
This commit is contained in:
1 parent
480c3ec769
commit
8c4cbc583f
5 files changed
+195
-53
No files matched your search
+4
-4
@@ -346,10 +346,10 @@ a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/test
|
|||||||
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
|
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
|
||||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||||
21ddb01b8f9c7fab60840d18103ef8de4716ff0c890cc1525b472b69af7916ec src/logging/error_runtime.rs
|
21ddb01b8f9c7fab60840d18103ef8de4716ff0c890cc1525b472b69af7916ec src/logging/error_runtime.rs
|
||||||
42b88cefef603fafa9eec3ba83a53069f01f571e98cf923210a6fe605e008e90 src/logging/format.rs
|
add1293410e87ddae85ba4b8bec57b56ff54415b8112576c450f85920ab6c38c src/logging/format.rs
|
||||||
f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/model.rs
|
f5815d778cc1785723faed4abb15f9880f22b6524b5289fe1e7158905aac0986 src/logging/model.rs
|
||||||
b90ee5c7badacd0141fba99a8109fced785a11abe11675c8687fa35e68f5e7b1 src/logging/runtime.rs
|
b9bef61d332df0dec47f15d864a7aaea240935c432bf935e40144916e37553bc src/logging/runtime.rs
|
||||||
c83a7b84a9d7226c200394c351c4b3d9c417869828c01a35d4b1297670ab50f2 src/logging/tests.rs
|
106f769f45a832079b170e1cf11d7e6a78adba354bb0a88bfb8e3aff14f8cce6 src/logging/tests.rs
|
||||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||||
fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs
|
fc8e99eac83c083d65101f6402aacbcc8392d66508c31566a50de3ebcb12e7da src/main/admin_runtime.rs
|
||||||
42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs
|
42a9197475823f67c1ca762951ede5d7a2f10124bd6ed4d1a3365f4297be894c src/main/bootstrap.rs
|
||||||
@@ -400,7 +400,7 @@ b9a2307c6aa7f16613d935508a61574dfee964020b1b5be86293df2d684615d8 src/proxy/requ
|
|||||||
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
|
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
|
||||||
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
|
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
|
||||||
a74815a0b46d1c94ffbf8cc3a8bbbc967daa4aec0981139d2d92624c10553026 src/proxy/types.rs
|
a74815a0b46d1c94ffbf8cc3a8bbbc967daa4aec0981139d2d92624c10553026 src/proxy/types.rs
|
||||||
b45cf5baf45e37915c8cfac43e845976f874f0aa5474f167fa943c0f38decade src/proxy/upstream_helpers.rs
|
1db11efc7c35b8654f88f3176d9bca341c3e625d055e6c7e888f953c04b5f573 src/proxy/upstream_helpers.rs
|
||||||
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
|
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
|
||||||
d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.rs
|
d7b4b809b12ee89293df45e9a64b1c9981fb3f89deaab3762f4a3ad3992dd7b1 src/proxy/web_cache_revalidation/tests.rs
|
||||||
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
|
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
|
||||||
|
|||||||
+56
-34
@@ -49,12 +49,28 @@ pub struct SinkStats {
|
|||||||
pub errors: u64,
|
pub errors: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// History owns entries only when there is no external sink. With a sink,
|
||||||
|
// both the queue and history share one immutable record instead of copying
|
||||||
|
// every allocated string on each request.
|
||||||
|
enum HistoryEntry {
|
||||||
|
Owned(LogEntry),
|
||||||
|
Shared(Arc<LogEntry>),
|
||||||
|
}
|
||||||
|
impl HistoryEntry {
|
||||||
|
fn entry(&self) -> &LogEntry {
|
||||||
|
match self {
|
||||||
|
Self::Owned(value) => value,
|
||||||
|
Self::Shared(value) => value.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Logs {
|
pub struct Logs {
|
||||||
entries: Mutex<VecDeque<LogEntry>>,
|
entries: Mutex<VecDeque<HistoryEntry>>,
|
||||||
pub dropped: AtomicU64,
|
pub dropped: AtomicU64,
|
||||||
settings: Arc<ArcSwap<Settings>>,
|
settings: Arc<ArcSwap<Settings>>,
|
||||||
live: broadcast::Sender<LogEntry>,
|
live: broadcast::Sender<LogEntry>,
|
||||||
sink: mpsc::Sender<LogEntry>,
|
sink: mpsc::Sender<Arc<LogEntry>>,
|
||||||
sink_dropped: Arc<AtomicU64>,
|
sink_dropped: Arc<AtomicU64>,
|
||||||
sink_sent: Arc<AtomicU64>,
|
sink_sent: Arc<AtomicU64>,
|
||||||
sink_errors: Arc<AtomicU64>,
|
sink_errors: Arc<AtomicU64>,
|
||||||
@@ -135,12 +151,25 @@ impl Logs {
|
|||||||
pub fn capture_country(&self) -> bool {
|
pub fn capture_country(&self) -> bool {
|
||||||
self.settings.load().capture_country
|
self.settings.load().capture_country
|
||||||
}
|
}
|
||||||
|
fn retain(&self, entry: HistoryEntry) {
|
||||||
|
let evicted = if let Ok(mut entries) = self.entries.try_lock() {
|
||||||
|
let evicted = if entries.len() == HISTORY_CAPACITY {
|
||||||
|
entries.pop_front()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
entries.push_back(entry);
|
||||||
|
evicted
|
||||||
|
} else {
|
||||||
|
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
// Evicted record destruction must happen outside the history mutex.
|
||||||
|
drop(evicted);
|
||||||
|
}
|
||||||
pub fn push(&self, entry: LogEntry) {
|
pub fn push(&self, entry: LogEntry) {
|
||||||
let has_viewers = self.live.receiver_count() != 0;
|
let has_viewers = self.live.receiver_count() != 0;
|
||||||
let has_sink = self.settings.load().enabled();
|
let sink_permit = if self.settings.load().enabled() {
|
||||||
// Reserve the bounded sink slot BEFORE cloning a log entry. Under
|
|
||||||
// saturation, the failed enqueue must not copy 30+ owned fields.
|
|
||||||
let sink_permit = if has_sink {
|
|
||||||
match self.sink.try_reserve() {
|
match self.sink.try_reserve() {
|
||||||
Ok(permit) => Some(permit),
|
Ok(permit) => Some(permit),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -151,38 +180,31 @@ impl Logs {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let mut external = Some(entry);
|
|
||||||
let history_entry = if has_viewers || sink_permit.is_some() {
|
|
||||||
external.as_ref().expect("entry").clone()
|
|
||||||
} else {
|
|
||||||
external.take().expect("entry")
|
|
||||||
};
|
|
||||||
let evicted = if let Ok(mut entries) = self.entries.try_lock() {
|
|
||||||
let evicted = if entries.len() == HISTORY_CAPACITY {
|
|
||||||
entries.pop_front()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
entries.push_back(history_entry);
|
|
||||||
evicted
|
|
||||||
} else {
|
|
||||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
|
||||||
None
|
|
||||||
};
|
|
||||||
// Destroy evicted strings outside of the history lock.
|
|
||||||
drop(evicted);
|
|
||||||
if let Some(entry) = external {
|
|
||||||
if has_viewers {
|
|
||||||
let _ = self.live.send(entry.clone());
|
|
||||||
}
|
|
||||||
if let Some(permit) = sink_permit {
|
if let Some(permit) = sink_permit {
|
||||||
permit.send(entry);
|
// One Arc allocation replaces a full LogEntry clone: no per-field
|
||||||
|
// String allocations for the active file/network logging path.
|
||||||
|
let shared = Arc::new(entry);
|
||||||
|
let live = has_viewers.then(|| shared.as_ref().clone());
|
||||||
|
self.retain(HistoryEntry::Shared(shared.clone()));
|
||||||
|
if let Some(live) = live {
|
||||||
|
let _ = self.live.send(live);
|
||||||
|
}
|
||||||
|
permit.send(shared);
|
||||||
|
} else {
|
||||||
|
// History-only operation retains the original no-Arc/no-copy path.
|
||||||
|
let live = has_viewers.then(|| entry.clone());
|
||||||
|
self.retain(HistoryEntry::Owned(entry));
|
||||||
|
if let Some(live) = live {
|
||||||
|
let _ = self.live.send(live);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn push_sink(&self, entry: LogEntry) {
|
fn push_sink(&self, entry: LogEntry) {
|
||||||
if self.settings.load().enabled() && self.sink.try_send(entry).is_err() {
|
if self.settings.load().enabled() {
|
||||||
self.sink_dropped.fetch_add(1, Ordering::Relaxed);
|
match self.sink.try_reserve() {
|
||||||
|
Ok(permit) => permit.send(Arc::new(entry)),
|
||||||
|
Err(_) => { self.sink_dropped.fetch_add(1, Ordering::Relaxed); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Persist transport/protocol failures without polluting the bounded Live Logs buffer.
|
/// Persist transport/protocol failures without polluting the bounded Live Logs buffer.
|
||||||
@@ -281,7 +303,7 @@ impl Logs {
|
|||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.take(limit.min(HISTORY_CAPACITY))
|
.take(limit.min(HISTORY_CAPACITY))
|
||||||
.cloned()
|
.map(|item| item.entry().clone())
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
|
|||||||
+65
-13
@@ -41,6 +41,63 @@ fn format_entry(entry: &LogEntry, format: &JsonFormat) -> Value {
|
|||||||
Value::Object(object)
|
Value::Object(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A request with ordinary values cannot match any redaction rule. Every
|
||||||
|
// supported rule (including private PEM and escaped JSON) contains one of
|
||||||
|
// these case-insensitive ASCII markers. If any marker is present, fall back
|
||||||
|
// to the original full recursive redaction, even for a false positive.
|
||||||
|
fn may_require_redaction(text: &str) -> bool {
|
||||||
|
let data = text.as_bytes();
|
||||||
|
for (i, byte) in data.iter().enumerate() {
|
||||||
|
let marker: &[u8] = match byte.to_ascii_lowercase() {
|
||||||
|
b'k' => b"key",
|
||||||
|
b'a' => b"auth",
|
||||||
|
b'p' => b"pass",
|
||||||
|
b't' => b"token",
|
||||||
|
b's' => b"secret",
|
||||||
|
b'c' => b"credential",
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
if data.get(i..i + marker.len()).is_some_and(|value| value.eq_ignore_ascii_case(marker)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_requires_redaction(entry: &LogEntry) -> bool {
|
||||||
|
[
|
||||||
|
entry.event, entry.protocol_error.unwrap_or(""), entry.reason.unwrap_or(""),
|
||||||
|
entry.error_detail.as_deref().unwrap_or(""), entry.error.as_deref().unwrap_or(""),
|
||||||
|
entry.local_addr.as_deref().unwrap_or(""), entry.tls_sni.as_deref().unwrap_or(""),
|
||||||
|
entry.sni.as_deref().unwrap_or(""), entry.authority.as_deref().unwrap_or(""),
|
||||||
|
entry.matched_by.unwrap_or(""), entry.vhost.as_deref().unwrap_or(""),
|
||||||
|
entry.route.as_deref().unwrap_or(""), entry.backend.as_deref().unwrap_or(""),
|
||||||
|
&entry.request_id, &entry.method, &entry.host, &entry.path,
|
||||||
|
entry.http_version, entry.transport, entry.websocket_bootstrap.unwrap_or(""),
|
||||||
|
entry.websocket_phase.unwrap_or(""), entry.websocket_error.unwrap_or(""),
|
||||||
|
entry.h3_reset_code.unwrap_or(""), entry.upstream_error.as_deref().unwrap_or(""),
|
||||||
|
entry.client_ip.as_deref().unwrap_or(""), entry.user_agent.as_deref().unwrap_or(""),
|
||||||
|
entry.country.as_deref().unwrap_or(""), entry.outcome, entry.cache, entry.cache_reason,
|
||||||
|
].iter().any(|value| may_require_redaction(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize into the existing batch buffer: no temporary Vec per record.
|
||||||
|
// The ordinary default format also skips the intermediate serde_json::Value
|
||||||
|
// tree and its many field allocations. Sensitive/custom records retain the
|
||||||
|
// exact previous formatting and recursive redaction behavior.
|
||||||
|
fn append_jsonl(entry: &LogEntry, format: &JsonFormat, buffer: &mut Vec<u8>) {
|
||||||
|
if format.fields.is_empty() && format.static_fields.is_empty()
|
||||||
|
&& !entry_requires_redaction(entry)
|
||||||
|
{
|
||||||
|
serde_json::to_writer(&mut *buffer, entry).expect("LogEntry JSON serialization");
|
||||||
|
} else {
|
||||||
|
let mut payload = format_entry(entry, format);
|
||||||
|
redact_value(&mut payload);
|
||||||
|
serde_json::to_writer(&mut *buffer, &payload).expect("log Value JSON serialization");
|
||||||
|
}
|
||||||
|
buffer.push(b'\n');
|
||||||
|
}
|
||||||
|
|
||||||
// Bound both write size and event count; never wait for the batch to fill.
|
// Bound both write size and event count; never wait for the batch to fill.
|
||||||
const FILE_BATCH_EVENTS: u64 = 64;
|
const FILE_BATCH_EVENTS: u64 = 64;
|
||||||
const FILE_BATCH_BYTES: usize = 128 * 1024;
|
const FILE_BATCH_BYTES: usize = 128 * 1024;
|
||||||
@@ -107,7 +164,7 @@ async fn open_access_file(path: PathBuf) -> Result<File> {
|
|||||||
Ok(File::from_std(file))
|
Ok(File::from_std(file))
|
||||||
}
|
}
|
||||||
async fn sink_loop(
|
async fn sink_loop(
|
||||||
mut rx: mpsc::Receiver<LogEntry>,
|
mut rx: mpsc::Receiver<Arc<LogEntry>>,
|
||||||
settings: Arc<ArcSwap<Settings>>,
|
settings: Arc<ArcSwap<Settings>>,
|
||||||
sent: Arc<AtomicU64>,
|
sent: Arc<AtomicU64>,
|
||||||
errors: Arc<AtomicU64>,
|
errors: Arc<AtomicU64>,
|
||||||
@@ -118,7 +175,7 @@ async fn sink_loop(
|
|||||||
let mut vhost_files: BTreeMap<PathBuf, File> = BTreeMap::new();
|
let mut vhost_files: BTreeMap<PathBuf, File> = BTreeMap::new();
|
||||||
let mut vhost_directory: Option<PathBuf> = None;
|
let mut vhost_directory: Option<PathBuf> = None;
|
||||||
let mut logstash: Option<LogstashState> = None;
|
let mut logstash: Option<LogstashState> = None;
|
||||||
let mut deferred_entry: Option<LogEntry> = None;
|
let mut deferred_entry: Option<Arc<LogEntry>> = None;
|
||||||
let mut file_batch = Vec::with_capacity(8 * 1024);
|
let mut file_batch = Vec::with_capacity(8 * 1024);
|
||||||
loop {
|
loop {
|
||||||
let entry = if let Some(entry) = deferred_entry.take() {
|
let entry = if let Some(entry) = deferred_entry.take() {
|
||||||
@@ -152,11 +209,7 @@ async fn sink_loop(
|
|||||||
let mut count = 0u64;
|
let mut count = 0u64;
|
||||||
let mut next = Some(entry);
|
let mut next = Some(entry);
|
||||||
while let Some(item) = next.take() {
|
while let Some(item) = next.take() {
|
||||||
let mut payload = format_entry(&item, &config.format);
|
append_jsonl(&item, &config.format, &mut file_batch);
|
||||||
redact_value(&mut payload);
|
|
||||||
let mut line = serde_json::to_vec(&payload).unwrap_or_default();
|
|
||||||
line.push(b'\n');
|
|
||||||
file_batch.extend_from_slice(&line);
|
|
||||||
count += 1;
|
count += 1;
|
||||||
if count >= FILE_BATCH_EVENTS || file_batch.len() >= FILE_BATCH_BYTES {
|
if count >= FILE_BATCH_EVENTS || file_batch.len() >= FILE_BATCH_BYTES {
|
||||||
break;
|
break;
|
||||||
@@ -186,14 +239,11 @@ async fn sink_loop(
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut payload = format_entry(&entry, &config.format);
|
// File/Logstash share one record; Loki-only needs a JSON object only
|
||||||
redact_value(&mut payload);
|
// when the HTTP request is actually built.
|
||||||
// File and Logstash consume the same JSONL record. Serialize once and
|
|
||||||
// keep no JSONL buffer at all for Loki-only configurations.
|
|
||||||
let mut jsonl_line = Vec::new();
|
let mut jsonl_line = Vec::new();
|
||||||
if config.file.enabled || config.logstash.enabled {
|
if config.file.enabled || config.logstash.enabled {
|
||||||
jsonl_line = serde_json::to_vec(&payload).unwrap_or_default();
|
append_jsonl(&entry, &config.format, &mut jsonl_line);
|
||||||
jsonl_line.push(b'\n');
|
|
||||||
}
|
}
|
||||||
let mut attempted = false;
|
let mut attempted = false;
|
||||||
let mut failed = false;
|
let mut failed = false;
|
||||||
@@ -308,6 +358,8 @@ async fn sink_loop(
|
|||||||
// serialized record when another sink is active; avoid a second
|
// serialized record when another sink is active; avoid a second
|
||||||
// full serde_json pass for every high-volume access event.
|
// full serde_json pass for every high-volume access event.
|
||||||
let event_json = if jsonl_line.is_empty() {
|
let event_json = if jsonl_line.is_empty() {
|
||||||
|
let mut payload = format_entry(&entry, &config.format);
|
||||||
|
redact_value(&mut payload);
|
||||||
payload.to_string()
|
payload.to_string()
|
||||||
} else {
|
} else {
|
||||||
String::from_utf8_lossy(&jsonl_line[..jsonl_line.len() - 1]).into_owned()
|
String::from_utf8_lossy(&jsonl_line[..jsonl_line.len() - 1]).into_owned()
|
||||||
|
|||||||
+69
-1
@@ -378,7 +378,7 @@ mod tests {
|
|||||||
let mut sentinel = entry();
|
let mut sentinel = entry();
|
||||||
sentinel.id = 6;
|
sentinel.id = 6;
|
||||||
let mut history = logs.entries.lock().unwrap();
|
let mut history = logs.entries.lock().unwrap();
|
||||||
history.push_back(sentinel.clone());
|
history.push_back(HistoryEntry::Owned(sentinel.clone()));
|
||||||
|
|
||||||
let mut item = entry();
|
let mut item = entry();
|
||||||
item.path = "/contention/€\"quoted\"".into();
|
item.path = "/contention/€\"quoted\"".into();
|
||||||
@@ -521,4 +521,72 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jsonl_fast_serialization_preserves_values_and_redaction() {
|
||||||
|
let clean = entry();
|
||||||
|
assert!(!entry_requires_redaction(&clean));
|
||||||
|
let mut token = clean.clone();
|
||||||
|
token.path = "/download?access_token=private&language=pl".into();
|
||||||
|
token.user_agent = Some("Authorization: Bearer example".into());
|
||||||
|
assert!(entry_requires_redaction(&token));
|
||||||
|
let mut error = clean.clone();
|
||||||
|
error.event = "protocol_error";
|
||||||
|
error.error_detail = Some("-----BEGIN PRIVATE KEY-----abc-----END PRIVATE KEY-----".into());
|
||||||
|
error.error = Some("{\"password\":\"secret\"}".into());
|
||||||
|
assert!(entry_requires_redaction(&error));
|
||||||
|
let mut mapped = JsonFormat::default();
|
||||||
|
mapped.fields.insert("request_path".into(), "path".into());
|
||||||
|
mapped.fields.insert("http_status".into(), "status".into());
|
||||||
|
mapped.static_fields.insert("service".into(), "edge".into());
|
||||||
|
let mut nested = JsonFormat::default();
|
||||||
|
nested.static_fields.insert("token".into(), "secret-value".into());
|
||||||
|
let defaults = JsonFormat::default();
|
||||||
|
for (record, format) in [
|
||||||
|
(&clean, &defaults),
|
||||||
|
(&token, &defaults),
|
||||||
|
(&error, &defaults),
|
||||||
|
(&token, &mapped),
|
||||||
|
(&clean, &nested),
|
||||||
|
] {
|
||||||
|
let mut previous = format_entry(record, format);
|
||||||
|
redact_value(&mut previous);
|
||||||
|
let mut jsonl = Vec::new();
|
||||||
|
append_jsonl(record, format, &mut jsonl);
|
||||||
|
assert_eq!(jsonl.last(), Some(&b'\n'));
|
||||||
|
let got: Value = serde_json::from_slice(&jsonl[..jsonl.len() - 1]).unwrap();
|
||||||
|
assert_eq!(got, previous);
|
||||||
|
}
|
||||||
|
assert!(may_require_redaction("?key_secret=value"));
|
||||||
|
assert!(may_require_redaction("\"PASSWORD\":\"value\""));
|
||||||
|
assert!(!may_require_redaction("/assets/home.html"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn active_sink_shares_immutable_record_with_history() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let path = directory.path().join("access.jsonl");
|
||||||
|
let mut settings = Settings::default();
|
||||||
|
settings.file.enabled = true;
|
||||||
|
settings.file.path = path.to_string_lossy().into_owned();
|
||||||
|
let shutdown = CancellationToken::new();
|
||||||
|
let logs = Logs::start(settings, shutdown.clone());
|
||||||
|
let sample = entry();
|
||||||
|
logs.push(sample.clone());
|
||||||
|
{
|
||||||
|
let history = logs.entries.lock().unwrap();
|
||||||
|
assert!(matches!(history.back(), Some(HistoryEntry::Shared(_))));
|
||||||
|
assert_eq!(history.back().unwrap().entry().id, sample.id);
|
||||||
|
}
|
||||||
|
assert_eq!(serde_json::to_value(logs.recent()).unwrap(), json!([sample]));
|
||||||
|
tokio::time::timeout(Duration::from_secs(10), async {
|
||||||
|
while logs.stats().sent == 0 {
|
||||||
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}).await.unwrap();
|
||||||
|
shutdown.cancel();
|
||||||
|
assert_eq!(logs.stats().queue_dropped, 0);
|
||||||
|
let jsonl = tokio::fs::read_to_string(path).await.unwrap();
|
||||||
|
assert_eq!(jsonl.lines().count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
|||||||
.headers()
|
.headers()
|
||||||
.get(USER_AGENT)
|
.get(USER_AGENT)
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.map(|value| value.chars().take(512).collect::<String>())
|
.map(|value| log_prefix(value, 512))
|
||||||
})
|
})
|
||||||
.flatten();
|
.flatten();
|
||||||
let (http_version, transport) = match request.version() {
|
let (http_version, transport) = match request.version() {
|
||||||
|
|||||||
Reference in new issue
Block a user