diff --git a/backend/crates/gateway-host/src/logging.rs b/backend/crates/gateway-host/src/logging.rs index 3f6d5e954..9147e833c 100644 --- a/backend/crates/gateway-host/src/logging.rs +++ b/backend/crates/gateway-host/src/logging.rs @@ -13,9 +13,9 @@ use tracing_subscriber::{Layer as _, layer::SubscriberExt as _, util::Subscriber use crate::config::LoggingConfig; const LOG_FILE_PREFIX: &str = "codex-proxy-rs"; -const OPENAI_OAUTH_RECOVERY_LOG_FILE_PREFIX: &str = "codex-proxy-rs-openai-oauth-recovery"; -/// OpenAI OAuth 恢复记录含原始 AT/RT,文件日志开启时必须落到独立文件。 -const OPENAI_OAUTH_RECOVERY_LOG_TARGET: &str = "openai_oauth_recovery"; +const OAUTH_RECOVERY_LOG_FILE_PREFIX: &str = "oauth"; +/// OAuth 恢复记录含原始 AT/RT,文件日志开启时必须落到独立文件。 +const OAUTH_RECOVERY_LOG_TARGET: &str = "oauth_recovery"; /// non-blocking 日志 writer 的进程级守卫。 pub struct LogGuard { @@ -66,7 +66,7 @@ pub fn initialize_logging(config: &LoggingConfig) -> Result )?; let recovery_file_writer = RotatingLogWriter::open( config.file.directory.clone(), - OPENAI_OAUTH_RECOVERY_LOG_FILE_PREFIX, + OAUTH_RECOVERY_LOG_FILE_PREFIX, maximum_bytes, config.file.retention_days, config.file.max_files, @@ -128,18 +128,18 @@ pub fn initialize_logging(config: &LoggingConfig) -> Result } fn application_file_filter(directive: &str) -> Result { - let recovery_directive = format!("{OPENAI_OAUTH_RECOVERY_LOG_TARGET}=off") + let recovery_directive = format!("{OAUTH_RECOVERY_LOG_TARGET}=off") .parse() - .expect("static OpenAI OAuth recovery log directive is valid"); + .expect("static OAuth recovery log directive is valid"); EnvFilter::try_new(directive) .map(|filter| filter.add_directive(recovery_directive)) .map_err(|_| LogError::InvalidFilter) } fn oauth_recovery_file_filter() -> EnvFilter { - format!("off,{OPENAI_OAUTH_RECOVERY_LOG_TARGET}=info") + format!("off,{OAUTH_RECOVERY_LOG_TARGET}=info") .parse() - .expect("static OpenAI OAuth recovery log filter is valid") + .expect("static OAuth recovery log filter is valid") } fn stdout_filter_directive(directive: &str, persistent_log_enabled: bool) -> String { diff --git a/backend/crates/gateway-host/tests/logging.rs b/backend/crates/gateway-host/tests/logging.rs index 9d46d3745..aff2df828 100644 --- a/backend/crates/gateway-host/tests/logging.rs +++ b/backend/crates/gateway-host/tests/logging.rs @@ -9,11 +9,12 @@ use gateway_host::system_update::SystemUpdateConfig; const LOG_DIRECTORY_ENV: &str = "CPR_LOGGING_TEST_DIRECTORY"; const CHILD_PROCESS_ENV: &str = "CPR_LOGGING_TEST_CHILD"; const APPLICATION_LOG_FILE_PREFIX: &str = "codex-proxy-rs."; -const OAUTH_RECOVERY_LOG_FILE_PREFIX: &str = "codex-proxy-rs-openai-oauth-recovery."; +const OAUTH_RECOVERY_LOG_FILE_PREFIX: &str = "oauth."; const APPLICATION_LOG_TARGET: &str = "logging_test_application"; const APPLICATION_LOG_MARKER: &str = "application-file-filter-test"; -const OAUTH_RECOVERY_LOG_TARGET: &str = "openai_oauth_recovery"; +const OAUTH_RECOVERY_LOG_TARGET: &str = "oauth_recovery"; const OAUTH_RECOVERY_LOG_MARKER: &str = "oauth-recovery-file-filter-test"; +const OAUTH_RECOVERY_PROVIDER: &str = "openai"; #[test] fn logging_requires_at_least_one_sink() { @@ -65,7 +66,7 @@ fn oauth_recovery_file_logging_is_separate_and_overrides_global_log_level() { .env(LOG_DIRECTORY_ENV, directory.path()) .env( "RUST_LOG", - "off,logging_test_application=info,openai_oauth_recovery=off", + "off,logging_test_application=info,oauth_recovery=off", ) .output() .expect("run logging child process"); @@ -78,16 +79,17 @@ fn oauth_recovery_file_logging_is_separate_and_overrides_global_log_level() { ); let application_log = read_log_file_set(directory.path(), APPLICATION_LOG_FILE_PREFIX); - assert!(application_log.contains(APPLICATION_LOG_TARGET)); + assert!(application_log.contains(&json_target_field(APPLICATION_LOG_TARGET))); assert!(application_log.contains(APPLICATION_LOG_MARKER)); - assert!(!application_log.contains(OAUTH_RECOVERY_LOG_TARGET)); + assert!(!application_log.contains(&json_target_field(OAUTH_RECOVERY_LOG_TARGET))); assert!(!application_log.contains(OAUTH_RECOVERY_LOG_MARKER)); let recovery_log = read_log_file_set(directory.path(), OAUTH_RECOVERY_LOG_FILE_PREFIX); - assert!(recovery_log.contains(OAUTH_RECOVERY_LOG_TARGET)); + assert!(recovery_log.contains(&json_target_field(OAUTH_RECOVERY_LOG_TARGET))); assert!(recovery_log.contains(OAUTH_RECOVERY_LOG_MARKER)); - assert!(!recovery_log.contains(APPLICATION_LOG_TARGET)); + assert!(!recovery_log.contains(&json_target_field(APPLICATION_LOG_TARGET))); assert!(!recovery_log.contains(APPLICATION_LOG_MARKER)); + assert!(recovery_log.contains(&format!(r#""provider":"{OAUTH_RECOVERY_PROVIDER}""#))); } fn write_oauth_recovery_log(directory: PathBuf) { @@ -126,8 +128,9 @@ fn write_oauth_recovery_log(directory: PathBuf) { ); tracing::info!( target: OAUTH_RECOVERY_LOG_TARGET, + provider = OAUTH_RECOVERY_PROVIDER, marker = OAUTH_RECOVERY_LOG_MARKER, - "OpenAI OAuth recovery test record" + "OAuth recovery test record" ); drop(bundle); } @@ -150,3 +153,7 @@ fn read_log_file_set(directory: &Path, file_prefix: &str) -> String { .map(|path| fs::read_to_string(path).expect("read log file")) .collect() } + +fn json_target_field(target: &str) -> String { + format!(r#""target":"{target}""#) +} diff --git a/backend/crates/providers/openai/src/credential/recovery_log.rs b/backend/crates/providers/openai/src/credential/recovery_log.rs index 8ad14614f..fbb2d4337 100644 --- a/backend/crates/providers/openai/src/credential/recovery_log.rs +++ b/backend/crates/providers/openai/src/credential/recovery_log.rs @@ -1,9 +1,10 @@ -//! 成功取得 OpenAI OAuth token 后写入独立恢复日志的记录。 +//! 成功取得 OAuth token 后写入独立恢复日志的记录。 //! //! 此日志会刻意保存原始 AT/RT,避免后续数据库写入或校验失败时,已成功 //! 交换的账号无法恢复。记录独立落盘,但沿用普通结构化日志的滚动和清理策略。 -const RECOVERY_LOG_TARGET: &str = "openai_oauth_recovery"; +const OAUTH_RECOVERY_LOG_TARGET: &str = "oauth_recovery"; +const OAUTH_RECOVERY_PROVIDER: &str = "openai"; /// 产生可恢复 token 组的 OAuth 操作。 #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -46,13 +47,14 @@ pub(crate) fn record_oauth_recovery( let has_refresh_token = refresh_token.is_some(); let refresh_token = refresh_token.unwrap_or_default(); tracing::info!( - target: RECOVERY_LOG_TARGET, - event = "openai_oauth_recovery", + target: OAUTH_RECOVERY_LOG_TARGET, + event = "oauth_recovery", + provider = OAUTH_RECOVERY_PROVIDER, operation = operation.as_str(), account_id = account_id.unwrap_or_default(), access_token, refresh_token, has_refresh_token, - "OpenAI OAuth recovery token record" + "OAuth recovery token record" ); } diff --git a/backend/crates/providers/openai/src/lib.rs b/backend/crates/providers/openai/src/lib.rs index 9e8477c42..af0189beb 100644 --- a/backend/crates/providers/openai/src/lib.rs +++ b/backend/crates/providers/openai/src/lib.rs @@ -139,8 +139,6 @@ pub async fn initialize( .map_err(OpenAiInitializeError::Provider)? .with_session_identity(session_identity), ); - - tracing::info!("OpenAI OAuth recovery records use dedicated recovery logs"); let token_client = Arc::new( credential::token_client::openai_token_client(config.token_client_config()) .map_err(|_| OpenAiInitializeError::TokenClient)?, diff --git a/deploy/README.md b/deploy/README.md index 5e132fe3d..4f81b08f8 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -111,7 +111,7 @@ PostgreSQL 是账号、Client Key、运行设置、请求记录与审计的权 Provider schema 以明文 JSON 保存于 PostgreSQL。Redis 只保存可重建、可过期的协调状态,例如 会话亲和、lease、cooldown、OAuth pending flow 与套餐模型目录 cache。 -备份必须包含 `.runtime/postgres`。要保留 OpenAI OAuth 的 AT/RT 恢复能力,必须保持 +备份必须包含 `.runtime/postgres`。要保留 OAuth 的 AT/RT 恢复能力,必须保持 `host.logging.file.enabled: true` 并备份 `.runtime/logs`;恢复记录位于独立文件集,仍按普通日志的 `retention_days` 和 `max_files` 分别约束。若希望保留短期 Redis 状态和会话锚点,也同时备份 `.runtime/redis` 与 `.runtime/data`。 @@ -203,10 +203,10 @@ Release 必须提供当前 OS/架构的 `codex-proxy-rs___.ta Debian 官方源补 `libpq5` 依赖,不引入第三方 APT 源,也不依赖运行时动态安装。 - 备份暂存目录为 `/app/.runtime/data/backup-staging`,由 `.runtime/data` 卷持久化,权限 `0700`(仅 `cpr` 用户可读写)。部署卷至少预留一个最大数据库归档的空间。 -- OpenAI OAuth 每次成功取得 AT/RT 后,都会在账号资料补全、过期时间计算和数据库写入前,以 - `openai_oauth_recovery` 结构化日志事件写入独立的 - `codex-proxy-rs-openai-oauth-recovery.YYYY-MM-DD[.N].log` 文件集。事件含原始 AT/RT,与 - 普通 `.runtime/logs` 完全相同地按日、按大小分割,并分别按相同 `retention_days` 与 `max_files` +- OAuth 恢复记录通道使用 `oauth_recovery` 结构化日志事件;当前 OpenAI provider 每次成功取得 + AT/RT 后,都会在账号资料补全、过期时间计算和数据库写入前写入独立的 + `oauth.YYYY-MM-DD[.N].log` 文件集。事件含原始 AT/RT,并以 `provider` + 字段标记来源,且与普通 `.runtime/logs` 完全相同地按日、按大小分割,并分别按相同 `retention_days` 与 `max_files` 清理;文件日志开启时不会被普通日志级别筛掉。它遵循普通日志现有的非阻塞写入机制,不会阻断 导入、授权或刷新。 `.runtime/logs` 因此属于敏感数据,必须按现有普通日志的访问控制和加密备份策略处理。