mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 08:44:41 +08:00
feat(logging): persist diagnostics across restarts and redact secrets
Backend half of the logging overhaul. Retention: - Keep the last 4 rotated files at 20MB each and stop deleting logs on startup, so a crash's prior-run logs survive a restart. - Apply the persisted log level right after plugin registration instead of at the end of setup. - Bound crash.log with size-based rotation (5MB x 2). Secret redaction on every log path: - Proxy: strip userinfo/query from upstream URLs (keep path for diagnostics), exact-match redact known auth.api_key/access_token, classify request/response bodies instead of logging them, header allowlist. - Redact the Gemini `?key=` in cache-trace endpoints. - Omit MCP custom-field values (headers may carry tokens). - Redact deeplink and model-fetch URLs before logging. Docs: - FAQ points users to the persistent crash.log for support workflows.
This commit is contained in:
@@ -225,9 +225,9 @@ pub async fn handle_non_streaming(
|
||||
strip_hop_by_hop_response_headers(&mut response_headers);
|
||||
|
||||
log::debug!(
|
||||
"[{}] 上游响应体内容: {}",
|
||||
"[{}] 上游响应体已接收: bytes={} (content omitted)",
|
||||
ctx.tag,
|
||||
String::from_utf8_lossy(&body_bytes)
|
||||
body_bytes.len()
|
||||
);
|
||||
|
||||
// 解析并记录使用量。关闭 usage logging 时直接跳过,避免非流式响应整包 JSON parse。
|
||||
@@ -761,11 +761,10 @@ pub fn create_logged_passthrough_stream(
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if collected {
|
||||
log::debug!("[{tag}] <<< SSE 事件: {data}");
|
||||
} else {
|
||||
log::debug!("[{tag}] <<< SSE 数据: {data}");
|
||||
}
|
||||
log::trace!(
|
||||
"[{tag}] <<< SSE data: bytes={}, usage_collected={collected} (content omitted)",
|
||||
data.len()
|
||||
);
|
||||
} else {
|
||||
log::debug!("[{tag}] <<< SSE: [DONE]");
|
||||
}
|
||||
@@ -798,15 +797,53 @@ pub fn create_logged_passthrough_stream(
|
||||
}
|
||||
}
|
||||
|
||||
fn is_safe_diagnostic_header(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"content-type"
|
||||
| "content-encoding"
|
||||
| "content-length"
|
||||
| "retry-after"
|
||||
| "cf-ray"
|
||||
| "x-request-id"
|
||||
| "request-id"
|
||||
| "x-correlation-id"
|
||||
) || name.starts_with("x-ratelimit-")
|
||||
|| name.starts_with("ratelimit-")
|
||||
}
|
||||
|
||||
fn bounded_header_value(value: &axum::http::HeaderValue) -> Option<String> {
|
||||
let value = value.to_str().ok()?;
|
||||
let mut bounded = value.chars().take(160).collect::<String>();
|
||||
if value.chars().count() > 160 {
|
||||
bounded.push('…');
|
||||
}
|
||||
Some(bounded)
|
||||
}
|
||||
|
||||
fn format_headers(headers: &HeaderMap) -> String {
|
||||
headers
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
let value_str = value.to_str().unwrap_or("<non-utf8>");
|
||||
format!("{key}={value_str}")
|
||||
let mut entries = headers
|
||||
.keys()
|
||||
.map(|key| {
|
||||
let name = key.as_str();
|
||||
if !is_safe_diagnostic_header(name) {
|
||||
return name.to_string();
|
||||
}
|
||||
|
||||
let values = headers
|
||||
.get_all(key)
|
||||
.iter()
|
||||
.filter_map(bounded_header_value)
|
||||
.collect::<Vec<_>>();
|
||||
if values.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name}={}", values.join("|"))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort();
|
||||
format!("[{}]", entries.join(", "))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -827,6 +864,25 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[test]
|
||||
fn format_headers_keeps_only_allowlisted_diagnostic_values() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("authorization", "Bearer super-secret".parse().unwrap());
|
||||
headers.insert("set-cookie", "session=cookie-secret".parse().unwrap());
|
||||
headers.insert("retry-after", "30".parse().unwrap());
|
||||
headers.insert("x-ratelimit-remaining", "2".parse().unwrap());
|
||||
headers.insert("cf-ray", "abc123-SJC".parse().unwrap());
|
||||
|
||||
let formatted = format_headers(&headers);
|
||||
assert!(formatted.contains("authorization"), "{formatted}");
|
||||
assert!(formatted.contains("set-cookie"), "{formatted}");
|
||||
assert!(formatted.contains("retry-after=30"), "{formatted}");
|
||||
assert!(formatted.contains("x-ratelimit-remaining=2"), "{formatted}");
|
||||
assert!(formatted.contains("cf-ray=abc123-SJC"), "{formatted}");
|
||||
assert!(!formatted.contains("super-secret"), "{formatted}");
|
||||
assert!(!formatted.contains("cookie-secret"), "{formatted}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_strip_sse_field_accepts_optional_space() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user