Feature/error request logging (#401)

* feat(proxy): add error mapper for HTTP status code mapping

- Add error_mapper.rs module to map ProxyError to HTTP status codes
- Implement map_proxy_error_to_status() for error classification
- Implement get_error_message() for user-friendly error messages
- Support all error types: upstream, timeout, connection, provider failures
- Include comprehensive unit tests for all mappings

* feat(proxy): enhance error logging with context support

- Add log_error_with_context() method for detailed error recording
- Support streaming flag, session_id, and provider_type fields
- Remove dead_code warning from log_error() method
- Enable comprehensive error request tracking in database

* feat(proxy): implement error capture and logging in all handlers

- Capture and log all failed requests in handle_messages (Claude)
- Capture and log all failed requests in handle_gemini (Gemini)
- Capture and log all failed requests in handle_responses (Codex)
- Capture and log all failed requests in handle_chat_completions (Codex)
- Record error status codes, messages, and latency for all failures
- Generate unique session_id for each request
- Support both streaming and non-streaming error scenarios

* style: fix clippy warnings and typescript errors

- Add allow(dead_code) for CircuitBreaker::get_state (reserved for future)
- Fix all uninlined format string warnings (27 instances)
- Use inline format syntax for better readability
- Fix unused import and parameter warnings in ProviderActions.tsx
- Achieve zero warnings in both Rust and TypeScript

* style: apply code formatting

- Remove trailing whitespace in misc.rs
- Add trailing comma in App.tsx
- Format multi-line className in ProviderCard.tsx

* feat(proxy): add settings button to proxy panel

Add configuration buttons in both running and stopped states to
provide easy access to proxy settings dialog.

* fix(speedtest): skip client build for invalid inputs

* chore(clippy): fix uninlined format args

* Merge branch 'main' into feature/error-request-logging
This commit is contained in:
YoVinchen
2025-12-16 21:02:08 +08:00
committed by GitHub
parent e6654bd7f9
commit ec20ff4d8c
13 changed files with 327 additions and 106 deletions
+8 -19
View File
@@ -142,8 +142,7 @@ impl ProxyService {
log::warn!("同步 Claude Token 到数据库失败: {e}");
} else {
log::info!(
"已同步 Claude Token 到数据库 (provider: {})",
provider_id
"已同步 Claude Token 到数据库 (provider: {provider_id})"
);
}
}
@@ -182,8 +181,7 @@ impl ProxyService {
log::warn!("同步 Codex Token 到数据库失败: {e}");
} else {
log::info!(
"已同步 Codex Token 到数据库 (provider: {})",
provider_id
"已同步 Codex Token 到数据库 (provider: {provider_id})"
);
}
}
@@ -222,8 +220,7 @@ impl ProxyService {
log::warn!("同步 Gemini Token 到数据库失败: {e}");
} else {
log::info!(
"已同步 Gemini Token 到数据库 (provider: {})",
provider_id
"已同步 Gemini Token 到数据库 (provider: {provider_id})"
);
}
}
@@ -354,7 +351,7 @@ impl ProxyService {
});
}
self.write_claude_live(&live_config)?;
log::info!("Claude Live 配置已接管,代理地址: {}", proxy_url);
log::info!("Claude Live 配置已接管,代理地址: {proxy_url}");
}
// Codex: 修改 config.toml 的 base_urlauth.json 的 OPENAI_API_KEY(代理会注入真实 Token
@@ -373,7 +370,7 @@ impl ProxyService {
live_config["config"] = json!(updated_config);
self.write_codex_live(&live_config)?;
log::info!("Codex Live 配置已接管,代理地址: {}", proxy_url);
log::info!("Codex Live 配置已接管,代理地址: {proxy_url}");
}
// Gemini: 修改 GOOGLE_GEMINI_BASE_URL,使用占位符替代真实 Token(代理会注入真实 Token
@@ -389,7 +386,7 @@ impl ProxyService {
});
}
self.write_gemini_live(&live_config)?;
log::info!("Gemini Live 配置已接管,代理地址: {}", proxy_url);
log::info!("Gemini Live 配置已接管,代理地址: {proxy_url}");
}
Ok(())
@@ -513,11 +510,7 @@ impl ProxyService {
.set_current_provider(app_type_enum.as_str(), provider_id)
.map_err(|e| format!("更新当前供应商失败: {e}"))?;
log::info!(
"代理模式:已切换 {} 的目标供应商为 {}",
app_type,
provider_id
);
log::info!("代理模式:已切换 {app_type} 的目标供应商为 {provider_id}");
Ok(())
}
@@ -721,11 +714,7 @@ impl ProxyService {
server
.reset_provider_circuit_breaker(provider_id, app_type)
.await;
log::info!(
"已重置 Provider {} (app: {}) 的熔断器",
provider_id,
app_type
);
log::info!("已重置 Provider {provider_id} (app: {app_type}) 的熔断器");
}
Ok(())
}
+43 -26
View File
@@ -31,40 +31,51 @@ impl SpeedtestService {
return Ok(vec![]);
}
let mut results: Vec<Option<EndpointLatency>> = vec![None; urls.len()];
let mut valid_targets = Vec::new();
for (idx, raw_url) in urls.into_iter().enumerate() {
let trimmed = raw_url.trim().to_string();
if trimmed.is_empty() {
results[idx] = Some(EndpointLatency {
url: raw_url,
latency: None,
status: None,
error: Some("URL 不能为空".to_string()),
});
continue;
}
match Url::parse(&trimmed) {
Ok(parsed_url) => valid_targets.push((idx, trimmed, parsed_url)),
Err(err) => {
results[idx] = Some(EndpointLatency {
url: trimmed,
latency: None,
status: None,
error: Some(format!("URL 无效: {err}")),
});
}
}
}
if valid_targets.is_empty() {
return Ok(results.into_iter().flatten().collect::<Vec<_>>());
}
let timeout = Self::sanitize_timeout(timeout_secs);
let client = Self::build_client(timeout)?;
let tasks = urls.into_iter().map(|raw_url| {
let tasks = valid_targets.into_iter().map(|(idx, trimmed, parsed_url)| {
let client = client.clone();
async move {
let trimmed = raw_url.trim().to_string();
if trimmed.is_empty() {
return EndpointLatency {
url: raw_url,
latency: None,
status: None,
error: Some("URL 不能为空".to_string()),
};
}
let parsed_url = match Url::parse(&trimmed) {
Ok(url) => url,
Err(err) => {
return EndpointLatency {
url: trimmed,
latency: None,
status: None,
error: Some(format!("URL 无效: {err}")),
};
}
};
// 先进行一次热身请求,忽略结果,仅用于复用连接/绕过首包惩罚。
let _ = client.get(parsed_url.clone()).send().await;
// 第二次请求开始计时,并将其作为结果返回。
let start = Instant::now();
match client.get(parsed_url).send().await {
let latency = match client.get(parsed_url).send().await {
Ok(resp) => EndpointLatency {
url: trimmed,
latency: Some(start.elapsed().as_millis()),
@@ -88,11 +99,17 @@ impl SpeedtestService {
error: Some(error_message),
}
}
}
};
(idx, latency)
}
});
Ok(join_all(tasks).await)
for (idx, latency) in join_all(tasks).await {
results[idx] = Some(latency);
}
Ok(results.into_iter().flatten().collect::<Vec<_>>())
}
fn build_client(timeout_secs: u64) -> Result<Client, AppError> {