diff --git a/backend/crates/gateway-host/src/config.rs b/backend/crates/gateway-host/src/config.rs index 59479d55c..bf188b751 100644 --- a/backend/crates/gateway-host/src/config.rs +++ b/backend/crates/gateway-host/src/config.rs @@ -72,7 +72,7 @@ fn missing_config_field(error: &config::ConfigError) -> Option { } /// Host 唯一拥有的进程配置 -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct HostConfig { #[serde(default)] pub timezone: gateway_core::time::DeploymentTimeZone, @@ -145,7 +145,7 @@ pub struct ListenConfig { } /// Host 结构化日志配置 -#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, PartialEq)] pub struct LoggingConfig { pub level: String, pub stdout: bool, @@ -157,17 +157,17 @@ pub struct LoggingConfig { /// 请求报文的留存窗口由 request_dump_retention_days 独立控制 #[serde(default)] pub request_dump: bool, - /// 完整报文的最少保留天数;按 UTC 日期整组清理,默认 1 天 + /// 完整报文的最少保留天数;支持小数,每天按 24 小时换算,默认 1 天 #[serde(default = "default_request_dump_retention_days")] - pub request_dump_retention_days: usize, + pub request_dump_retention_days: f64, } fn default_file_retention_days() -> usize { 7 } -fn default_request_dump_retention_days() -> usize { - 1 +fn default_request_dump_retention_days() -> f64 { + 1.0 } impl LoggingConfig { @@ -190,14 +190,19 @@ impl LoggingConfig { "host.logging.file.max_file_size_mb", )); } - if self.request_dump_retention_days == 0 { - return Err(ConfigError::InvalidField( - "host.logging.request_dump_retention_days", - )); - } + self.request_dump_retention()?; resolve_relative_path(source_dir, &mut self.file.directory); Ok(()) } + + pub(crate) fn request_dump_retention(&self) -> Result { + Duration::try_from_secs_f64(self.request_dump_retention_days * 86_400.0) + .ok() + .filter(|duration| !duration.is_zero()) + .ok_or(ConfigError::InvalidField( + "host.logging.request_dump_retention_days", + )) + } } /// Host 文件日志配置 diff --git a/backend/crates/gateway-host/src/logging/mod.rs b/backend/crates/gateway-host/src/logging/mod.rs index 05c02799f..09f0b4d80 100644 --- a/backend/crates/gateway-host/src/logging/mod.rs +++ b/backend/crates/gateway-host/src/logging/mod.rs @@ -15,7 +15,7 @@ mod sink; mod writer; use sink::{FileLogGuard, FileLogSink, LogHealth}; -use writer::RotatingLogWriter; +use writer::{LogRetention, RotatingLogWriter}; // 文件前缀统一为 codex-proxy-rs-;专用 tracing target 使用 snake_case const APPLICATION_LOG_FILE_PREFIX: &str = "codex-proxy-rs-application"; @@ -45,9 +45,11 @@ pub enum LogError { AlreadyInitialized, #[error("logging size limit is too large")] SizeOverflow, + #[error(transparent)] + InvalidConfig(#[from] crate::config::ConfigError), } -/// 初始化日志;文件按完整日期留存,不以分片数量淘汰窗口内记录 +/// 初始化日志;按各通道的保留窗口清理,不以分片数量淘汰记录 pub fn initialize_logging( config: &LoggingConfig, timezone: gateway_core::time::DeploymentTimeZone, @@ -74,7 +76,7 @@ pub fn initialize_logging( let (writer, guard) = create_file_writer( config, timezone, - config.file.retention_days, + LogRetention::CalendarDays(config.file.retention_days), APPLICATION_LOG_FILE_PREFIX, "gateway-log-application-file", Arc::clone(&health), @@ -88,7 +90,7 @@ pub fn initialize_logging( let (writer, guard) = create_file_writer( config, timezone, - config.file.retention_days, + LogRetention::CalendarDays(config.file.retention_days), OAUTH_RECOVERY_LOG_FILE_PREFIX, "gateway-log-oauth-recovery-file", Arc::clone(&health), @@ -102,7 +104,7 @@ pub fn initialize_logging( let (writer, guard) = create_file_writer( config, timezone, - config.request_dump_retention_days, + LogRetention::Elapsed(config.request_dump_retention()?), REQUEST_DUMP_LOG_FILE_PREFIX, "gateway-log-request-dump-file", Arc::clone(&health), @@ -220,7 +222,7 @@ impl LogGuard { fn create_file_writer( config: &LoggingConfig, timezone: gateway_core::time::DeploymentTimeZone, - retention_days: usize, + retention: LogRetention, prefix: &'static str, thread_name: &'static str, health: Arc, @@ -234,7 +236,7 @@ fn create_file_writer( config.file.directory.clone(), prefix, maximum_bytes, - retention_days, + retention, Arc::clone(&health), timezone, )?; diff --git a/backend/crates/gateway-host/src/logging/writer.rs b/backend/crates/gateway-host/src/logging/writer.rs index 70743a727..397906605 100644 --- a/backend/crates/gateway-host/src/logging/writer.rs +++ b/backend/crates/gateway-host/src/logging/writer.rs @@ -1,22 +1,30 @@ -//! 按大小轮转、压缩已关闭分片、按部署时区自然日期组清理 +//! 按大小与时间轮转、压缩已关闭分片,按自然日期或实际时长清理 use std::collections::BTreeMap; use std::fs::{self, File, OpenOptions}; use std::io::{self, BufWriter, Write}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::time::{Duration, Instant, SystemTime}; use chrono::{NaiveDate, Utc}; use flate2::{Compression, write::GzEncoder}; use super::sink::LogHealth; +#[derive(Clone, Copy)] +pub(super) enum LogRetention { + CalendarDays(usize), + Elapsed(Duration), +} + pub(super) struct RotatingLogWriter { timezone: gateway_core::time::DeploymentTimeZone, directory: PathBuf, prefix: &'static str, maximum_bytes: u64, - retention_days: usize, + retention: LogRetention, + opened_at: Instant, date: NaiveDate, segment: usize, bytes_written: u64, @@ -29,19 +37,23 @@ impl RotatingLogWriter { directory: PathBuf, prefix: &'static str, maximum_bytes: u64, - retention_days: usize, + retention: LogRetention, health: Arc, timezone: gateway_core::time::DeploymentTimeZone, ) -> io::Result { fs::create_dir_all(&directory)?; let date = timezone.local(Utc::now()).date_naive(); - cleanup_log_files(&directory, prefix, date, retention_days, timezone)?; + cleanup_log_files(&directory, prefix, retention, timezone, None)?; let latest = managed_log_files(&directory, prefix)? .into_iter() .filter(|entry| entry.date == date) .max_by_key(|entry| (entry.segment, entry.compressed)); let (segment, bytes_written) = match latest { - Some(entry) if !entry.compressed && entry.path.metadata()?.len() < maximum_bytes => { + Some(entry) + if matches!(retention, LogRetention::CalendarDays(_)) + && !entry.compressed + && entry.path.metadata()?.len() < maximum_bytes => + { (entry.segment, entry.path.metadata()?.len()) } Some(entry) => (next_segment(entry.segment)?, 0), @@ -54,7 +66,8 @@ impl RotatingLogWriter { directory, prefix, maximum_bytes, - retention_days, + retention, + opened_at: Instant::now(), date, segment, bytes_written, @@ -78,7 +91,14 @@ impl RotatingLogWriter { let day_changed = date != self.date; let size_exceeded = self.bytes_written > 0 && self.bytes_written.saturating_add(incoming_bytes as u64) > self.maximum_bytes; - if !day_changed && !size_exceeded { + // 小流量转储也需及时关闭分片,避免小数保留期仍要等自然日切换才能生效 + let age_exceeded = match self.retention { + LogRetention::CalendarDays(_) => false, + LogRetention::Elapsed(duration) => { + self.opened_at.elapsed() >= duration.min(Duration::from_secs(3600)) + } + }; + if !day_changed && !size_exceeded && !age_exceeded { return Ok(()); } let previous = self @@ -104,6 +124,7 @@ impl RotatingLogWriter { self.date = date; self.segment = segment; self.bytes_written = self.file.get_ref().metadata()?.len(); + self.opened_at = Instant::now(); if let Err(error) = compress_log_file(&previous) { self.health.maintenance_failed(error.kind()); } @@ -111,9 +132,9 @@ impl RotatingLogWriter { if let Err(error) = cleanup_log_files( &self.directory, self.prefix, - date, - self.retention_days, + self.retention, self.timezone, + Some(&path), ) { self.health.maintenance_failed(error.kind()); } @@ -203,17 +224,38 @@ fn managed_log_files(directory: &Path, prefix: &str) -> io::Result, ) -> io::Result<()> { + let files = managed_log_files(directory, prefix)?; + let retention_days = match retention { + LogRetention::CalendarDays(days) => days, + LogRetention::Elapsed(duration) => { + let now = SystemTime::now(); + for entry in files { + if active_path == Some(entry.path.as_path()) { + continue; + } + let modified = entry.path.metadata()?.modified()?; + if now + .duration_since(modified) + .is_ok_and(|age| age >= duration) + { + fs::remove_file(entry.path)?; + } + } + return Ok(()); + } + }; + let today = timezone.local(Utc::now()).date_naive(); let days = u64::try_from(retention_days).map_err(|_| io::Error::other("log retention overflow"))?; let cutoff = today .checked_sub_days(chrono::Days::new(days)) .unwrap_or(NaiveDate::MIN); let mut dates = BTreeMap::>::new(); - for entry in managed_log_files(directory, prefix)? { + for entry in files { dates.entry(entry.date).or_default().push(entry.path); } for (date, paths) in dates { diff --git a/backend/crates/gateway-host/tests/config.rs b/backend/crates/gateway-host/tests/config.rs index 2b29913a7..efc99bfc6 100644 --- a/backend/crates/gateway-host/tests/config.rs +++ b/backend/crates/gateway-host/tests/config.rs @@ -146,7 +146,7 @@ fn logging_rejects_zero_retention_windows() { for request_dump in [false, true] { let mut config = valid_config(); if request_dump { - config.logging.request_dump_retention_days = 0; + config.logging.request_dump_retention_days = 0.0; } else { config.logging.file.retention_days = 0; } @@ -168,7 +168,7 @@ fn logging_defaults_to_time_retention_and_ignores_removed_count_retention() { "file": { "enabled": true, "directory": "logs", "max_file_size_mb": 20 } }); let config: LoggingConfig = serde_json::from_value(value.clone()).unwrap(); - assert_eq!(config.request_dump_retention_days, 1); + assert_eq!(config.request_dump_retention_days, 1.0); assert_eq!(config.file.retention_days, 7); assert!(!config.oauth_recovery); value["file"]["max_files"] = serde_json::json!(20); @@ -178,6 +178,55 @@ fn logging_defaults_to_time_retention_and_ignores_removed_count_retention() { ); } +#[test] +fn request_dump_retention_accepts_integer_and_fractional_yaml_days() { + for (input, expected) in [("1", 1.0), ("0.5", 0.5), ("0.25", 0.25), ("1.5", 1.5)] { + let yaml = format!( + "level: info\nstdout: true\nrequest_dump_retention_days: {input}\nfile:\n enabled: true\n directory: logs\n max_file_size_mb: 20\n" + ); + let mut config = valid_config(); + config.logging = config::Config::builder() + .add_source(config::File::from_str(&yaml, config::FileFormat::Yaml)) + .build() + .unwrap() + .try_deserialize() + .unwrap(); + config + .resolve_and_validate( + std::path::Path::new("/srv/gateway"), + std::path::Path::new("/srv/gateway/web/dist"), + ) + .unwrap(); + assert_eq!(config.logging.request_dump_retention_days, expected); + } +} + +#[test] +fn request_dump_retention_rejects_invalid_durations() { + for days in [ + 0.0, + -0.5, + f64::NAN, + f64::INFINITY, + f64::NEG_INFINITY, + f64::MAX, + 1e-20, + ] { + let mut config = valid_config(); + config.logging.request_dump_retention_days = days; + let error = config + .resolve_and_validate( + std::path::Path::new("/srv/gateway"), + std::path::Path::new("/srv/gateway/web/dist"), + ) + .unwrap_err(); + assert!(matches!( + error, + gateway_host::ConfigError::InvalidField("host.logging.request_dump_retention_days") + )); + } +} + #[test] fn dedicated_file_channels_can_be_enabled_without_application_sinks() { for oauth_recovery in [false, true] { @@ -220,7 +269,7 @@ fn valid_config() -> HostConfig { }, oauth_recovery: false, request_dump: false, - request_dump_retention_days: 1, + request_dump_retention_days: 1.0, }, system_update, worker_shutdown_timeout_seconds: 30, diff --git a/backend/crates/gateway-host/tests/logging/mod.rs b/backend/crates/gateway-host/tests/logging/mod.rs index 7ea7885f4..93da4c051 100644 --- a/backend/crates/gateway-host/tests/logging/mod.rs +++ b/backend/crates/gateway-host/tests/logging/mod.rs @@ -48,7 +48,7 @@ fn logging_requires_at_least_one_sink() { }, oauth_recovery: false, request_dump: false, - request_dump_retention_days: 1, + request_dump_retention_days: 1.0, }, system_update: SystemUpdateConfig::default(), worker_shutdown_timeout_seconds: 30, @@ -325,7 +325,7 @@ fn logging_config(directory: PathBuf, request_dump: bool) -> HostConfig { }, oauth_recovery: true, request_dump, - request_dump_retention_days: 1, + request_dump_retention_days: 1.0, }, system_update: SystemUpdateConfig::default(), worker_shutdown_timeout_seconds: 30, diff --git a/backend/crates/gateway-host/tests/logging/writer.rs b/backend/crates/gateway-host/tests/logging/writer.rs index 3e2b70e9b..8272af6af 100644 --- a/backend/crates/gateway-host/tests/logging/writer.rs +++ b/backend/crates/gateway-host/tests/logging/writer.rs @@ -1,4 +1,4 @@ -//! 验证日志压缩、轮转与重启后按日期保留完整记录 +//! 验证日志压缩、轮转,以及重启后的自然日与小数天数保留窗口 use super::*; use std::time::SystemTime; @@ -10,12 +10,11 @@ fn retention_preserves_complete_dates_across_compression_rotation_and_restart() if env::var_os(CHILD_PROCESS_ENV).is_some() { with_file_logging( PathBuf::from(env::var_os(LOG_DIRECTORY_ENV).unwrap()), - true, + false, || { let payload = "x".repeat(1024 * 1024); // 三条记录已跨过 1 MiB 轮转边界;保留日期的 25 个分段由下方单独构造 for sequence in 0..ROTATION_RECORDS { - tracing::info!(target: REQUEST_DUMP_LOG_TARGET, sequence, payload, "retention record"); tracing::info!(target: APPLICATION_LOG_TARGET, sequence, payload, "retention record"); tracing::info!(target: OAUTH_RECOVERY_LOG_TARGET, sequence, payload, "retention record"); } @@ -32,7 +31,6 @@ fn retention_preserves_complete_dates_across_compression_rotation_and_restart() for (prefix, days) in [ (APPLICATION_LOG_FILE_PREFIX, 7), (OAUTH_RECOVERY_LOG_FILE_PREFIX, 7), - (REQUEST_DUMP_LOG_FILE_PREFIX, 1), ] { let boundary = today - chrono::Days::new(days); for segment in 1..=25 { @@ -90,11 +88,7 @@ fn retention_preserves_complete_dates_across_compression_rotation_and_restart() ); assert!(unrelated.exists()); - for prefix in [ - APPLICATION_LOG_FILE_PREFIX, - OAUTH_RECOVERY_LOG_FILE_PREFIX, - REQUEST_DUMP_LOG_FILE_PREFIX, - ] { + for prefix in [APPLICATION_LOG_FILE_PREFIX, OAUTH_RECOVERY_LOG_FILE_PREFIX] { let body = read_log_file_set(directory.path(), prefix); let mut sequences = [0; ROTATION_RECORDS]; let mut boundaries = std::collections::BTreeSet::new(); @@ -141,6 +135,151 @@ fn retention_preserves_complete_dates_across_compression_rotation_and_restart() } } +#[test] +fn request_dump_retention_uses_elapsed_time_across_compression_and_restart() { + const RETENTION_ENV: &str = "CPR_LOGGING_TEST_RETENTION_DAYS"; + if env::var_os(CHILD_PROCESS_ENV).is_some() { + let mut config = + logging_config(PathBuf::from(env::var_os(LOG_DIRECTORY_ENV).unwrap()), true); + config.logging.request_dump_retention_days = + env::var(RETENTION_ENV).unwrap().parse().unwrap(); + with_logging(config, || { + let payload = "x".repeat(1024 * 1024); + for sequence in 0..ROTATION_RECORDS { + tracing::info!(target: REQUEST_DUMP_LOG_TARGET, sequence, payload, "elapsed retention record"); + } + }); + return; + } + + for days in [0.25, 0.5, 1.0, 1.5] { + let directory = tempfile::tempdir().unwrap(); + let now = chrono::Utc::now(); + let cutoff = now - chrono::Duration::seconds((days * 86_400.0) as i64); + let mut retained = Vec::new(); + let mut expired = Vec::new(); + // 同一日期内既有过期文件也有有效文件,文件名日期不覆盖实际修改时间 + for (segment, modified, keep) in [ + (0, cutoff - chrono::Duration::minutes(1), false), + (1, cutoff + chrono::Duration::minutes(1), true), + (2, now + chrono::Duration::hours(1), true), + ] { + let path = directory.path().join(format!( + "{REQUEST_DUMP_LOG_FILE_PREFIX}{}.{segment}.log", + now.date_naive() + )); + fs::write(&path, format!("seed-{segment}\n")).unwrap(); + fs::File::open(&path) + .unwrap() + .set_modified(modified.into()) + .unwrap(); + if keep { + let modified = path.metadata().unwrap().modified().unwrap(); + retained.push((path, modified)); + } else { + expired.push(path); + } + } + // 归档的修改时间也参与清理,压缩不能重置保留窗口 + let archive = directory.path().join(format!( + "{REQUEST_DUMP_LOG_FILE_PREFIX}{}.3.log.gz", + now.date_naive() - chrono::Days::new(3) + )); + let mut encoder = flate2::write::GzEncoder::new( + fs::File::create(&archive).unwrap(), + flate2::Compression::fast(), + ); + std::io::Write::write_all(&mut encoder, b"expired archive\n").unwrap(); + encoder + .finish() + .unwrap() + .set_modified((cutoff - chrono::Duration::hours(1)).into()) + .unwrap(); + expired.push(archive); + let unrelated = directory.path().join("unmanaged.log"); + fs::write(&unrelated, "unmanaged\n").unwrap(); + + for run in 1..=2 { + let output = Command::new(env::current_exe().unwrap()) + .args(["--exact", "logging::writer::request_dump_retention_uses_elapsed_time_across_compression_and_restart"]) + .env(CHILD_PROCESS_ENV, "1") + .env(LOG_DIRECTORY_ENV, directory.path()) + .env(LOG_TIMEZONE_ENV, "America/New_York") + .env(RETENTION_ENV, days.to_string()) + .env("RUST_LOG", "off") + .output().unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + for (path, modified) in &retained { + let archive = path.with_extension("log.gz"); + assert!(archive.exists()); + assert_eq!(archive.metadata().unwrap().modified().unwrap(), *modified); + } + assert!( + expired + .iter() + .all(|path| !path.exists() && !path.with_extension("log.gz").exists()) + ); + assert!(unrelated.exists()); + let body = read_log_file_set(directory.path(), REQUEST_DUMP_LOG_FILE_PREFIX); + assert!(body.contains("seed-1\n") && body.contains("seed-2\n")); + assert_eq!( + body.matches("elapsed retention record").count(), + run * ROTATION_RECORDS + ); + } + } +} + +#[test] +fn request_dump_rotates_small_files_when_the_time_window_elapses() { + if env::var_os(CHILD_PROCESS_ENV).is_some() { + let directory = PathBuf::from(env::var_os(LOG_DIRECTORY_ENV).unwrap()); + let mut config = logging_config(directory.clone(), true); + config.logging.request_dump_retention_days = 1.0 / 86_400.0; + with_logging(config, || { + tracing::info!(target: REQUEST_DUMP_LOG_TARGET, "before retention window"); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); + loop { + let body = read_log_file_set(&directory, REQUEST_DUMP_LOG_FILE_PREFIX); + if body.contains("before retention window") { + break; + } + assert!( + std::time::Instant::now() < deadline, + "first record was not flushed" + ); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + std::thread::sleep(std::time::Duration::from_millis(1100)); + tracing::info!(target: REQUEST_DUMP_LOG_TARGET, "after retention window"); + }); + return; + } + let directory = tempfile::tempdir().unwrap(); + let output = Command::new(env::current_exe().unwrap()) + .args([ + "--exact", + "logging::writer::request_dump_rotates_small_files_when_the_time_window_elapses", + ]) + .env(CHILD_PROCESS_ENV, "1") + .env(LOG_DIRECTORY_ENV, directory.path()) + .env("RUST_LOG", "off") + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let body = read_log_file_set(directory.path(), REQUEST_DUMP_LOG_FILE_PREFIX); + assert!(!body.contains("before retention window")); + assert!(body.contains("after retention window")); +} + fn seed_log(path: &Path, date: chrono::NaiveDate, body: &str) { fs::write(path, body).unwrap(); let modified = gateway_core::time::DeploymentTimeZone::default() diff --git a/backend/crates/gateway-host/tests/support/host.rs b/backend/crates/gateway-host/tests/support/host.rs index ce30ce359..d0819381b 100644 --- a/backend/crates/gateway-host/tests/support/host.rs +++ b/backend/crates/gateway-host/tests/support/host.rs @@ -56,7 +56,7 @@ pub fn configuration(directory: &Path) -> HostConfig { }, oauth_recovery: false, request_dump: false, - request_dump_retention_days: 1, + request_dump_retention_days: 1.0, }, system_update: Default::default(), worker_shutdown_timeout_seconds: 1, diff --git a/deploy/README.md b/deploy/README.md index ab1ab8bff..82284fe8b 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -457,19 +457,26 @@ Provider schema 以明文 JSON 保存于 PostgreSQL。Redis 只保存可重建 | 文件集 | 保留配置 | 默认完整窗口 | | --- | --- | --- | | 普通日志、OAuth 恢复日志 | `host.logging.file.retention_days` | 前 7 个完整自然日及当天 | -| 全量请求/响应报文 | `host.logging.request_dump_retention_days` | 前 1 个完整自然日及当天 | +| 全量请求/响应报文 | `host.logging.request_dump_retention_days` | 最近 24 小时 | 文件名统一为 `codex-proxy-rs-<类别>.YYYY-MM-DD[.N].log[.gz]`,类别分别为 `application`、`oauth-recovery`、`request-dump`。专用 tracing target 为 `oauth_recovery` 和 `request_dump`;普通日志保留各 Rust 模块的 target,便于按模块过滤。 程序只管理上述规范名称的日志,其他命名的文件由运维手动清理。 -日志时间戳采用部署时区并携带数字偏移,文件按该时区的日期轮转和整组保留:例如 9 月 8 日配置 1 天,会保留 9 月 7 日全天及 9 月 8 日的所有分片, +日志时间戳采用部署时区并携带数字偏移。普通日志与 OAuth 恢复日志按该时区的日期轮转和整组保留:例如 9 月 8 日配置 1 天,会保留 9 月 7 日全天及 9 月 8 日的所有分片, 到 9 月 9 日才允许清理 9 月 7 日,避免留下半天日志。夏令时日期允许为 23 或 25 小时。切换时区不重命名历史文件; 按文件名日期与部署时区中的实际修改日期取较近值,旧日期分片近期被写入时整组延后删除。 + +请求转储保留期支持正小数,按每天 24 小时换算,例如 `0.5` 为 12 小时、`0.25` 为 6 小时。 +已关闭分片以最后修改时间判断过期,整片删除,不受时区或夏令时影响。 +时间轮转间隔取一小时与保留期中的较小值,在下一条记录写入时触发。 +实际保存时间可能长于配置值,取决于分片的最后写入时间和下次清理时机 + `max_file_size_mb` 仅决定分片大小(默认 20 MiB),不决定保存时长,单条大记录不会被截断。 关闭的分片压缩为 `.log.gz`;成功压缩、同步并发布归档后才删除原文件,保留原修改时间。 -清理发生在启动和轮转时;空闲期间过期文件可能暂时多保留。检索时须同时读取 `.log` 与 `.log.gz` +清理发生在启动和轮转时;空闲期间过期文件可能暂时多保留。请求转储重启时新建分片,避免反复追加延后旧记录的过期点。 +检索时须同时读取 `.log` 与 `.log.gz` 报文开关为 `host.logging.request_dump`,默认关闭;开启时原始报文按块写入独立文件, 缓冲拒收或写入失败可能造成缺块,需结合 `file_logging` 健康状态判断完整性。 diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index c446955ca..ac683e2c1 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -24,6 +24,7 @@ host: oauth_recovery: false # 将完整请求与响应头、正文写入独立文件;包含凭据,仅排障时开启。 request_dump: false + # 按实际时长保留,支持正小数天数,例如 0.5 为 12 小时 request_dump_retention_days: 1 file: enabled: true