fix(logging): generalize OAuth recovery logs

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-08-11 14:55:59 +08:00
co-authored by Codex
parent f4b75c42ed
commit 93abe8efcd
5 changed files with 35 additions and 28 deletions
+8 -8
View File
@@ -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<LogGuard, LogError>
)?;
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<LogGuard, LogError>
}
fn application_file_filter(directive: &str) -> Result<EnvFilter, LogError> {
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 {
+15 -8
View File
@@ -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}""#)
}
@@ -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"
);
}
@@ -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)?,
+5 -5
View File
@@ -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_<version>_<os>_<arch>.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` 因此属于敏感数据,必须按现有普通日志的访问控制和加密备份策略处理。