feat(tunnel/usage): proxy writer 双优先级队列、hub 连接压力感知选择、usage 请求记录级别控制及 trace 页面 proxy timing 增强

This commit is contained in:
fawney19
2026-04-16 20:14:50 +08:00
parent 65cd9dc3e5
commit 0dce667019
13 changed files with 1229 additions and 77 deletions

View File

@@ -30,7 +30,9 @@ pub use report_context::{
build_locally_actionable_report_context_from_request_candidate,
build_locally_actionable_report_context_from_video_task, report_context_is_locally_actionable,
};
pub use runtime::{UsageBillingEventEnricher, UsageRuntime, UsageRuntimeAccess};
pub use runtime::{
UsageBillingEventEnricher, UsageRequestRecordLevel, UsageRuntime, UsageRuntimeAccess,
};
pub use settlement::{settle_usage_if_needed, UsageSettlementWriter};
pub use standardized_usage::StandardizedUsage;
pub use usage_mapper::{map_usage, map_usage_from_response, UsageMapper};

View File

@@ -24,12 +24,24 @@ pub trait UsageBillingEventEnricher: Send + Sync {
async fn enrich_usage_event(&self, event: &mut UsageEvent) -> Result<(), DataLayerError>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum UsageRequestRecordLevel {
Basic,
#[default]
Full,
}
#[async_trait]
pub trait UsageRuntimeAccess:
UsageRecordWriter + UsageSettlementWriter + UsageBillingEventEnricher + Send + Sync
{
fn has_usage_writer(&self) -> bool;
fn has_usage_worker_runner(&self) -> bool;
fn usage_worker_runner(&self) -> Option<RedisStreamRunner>;
async fn request_record_level(&self) -> Result<UsageRequestRecordLevel, DataLayerError> {
Ok(UsageRequestRecordLevel::Full)
}
}
#[derive(Debug, Clone)]
@@ -197,6 +209,7 @@ impl UsageRuntime {
spawn_on_usage_background_runtime(boxed_usage_task(async move {
match build_sync_terminal_usage_event_offthread(input).await {
Ok(mut event) => {
apply_request_record_level_from_data(&data, &mut event).await;
if let Err(err) = data.enrich_usage_event(&mut event).await {
warn!(
event_name = "usage_sync_terminal_billing_enrichment_failed",
@@ -244,6 +257,7 @@ impl UsageRuntime {
spawn_on_usage_background_runtime(boxed_usage_task(async move {
match build_stream_terminal_usage_event_offthread(input).await {
Ok(mut event) => {
apply_request_record_level_from_data(&data, &mut event).await;
if let Err(err) = data.enrich_usage_event(&mut event).await {
warn!(
event_name = "usage_stream_terminal_billing_enrichment_failed",
@@ -289,6 +303,7 @@ impl UsageRuntime {
if !self.is_enabled() {
return;
}
apply_request_record_level_from_data(data, &mut event).await;
if let Err(err) = data.enrich_usage_event(&mut event).await {
warn!(
event_name = "usage_terminal_billing_enrichment_failed",
@@ -431,6 +446,40 @@ fn join_error_to_data_layer(err: tokio::task::JoinError) -> DataLayerError {
DataLayerError::UnexpectedValue(format!("usage builder task join failed: {err}"))
}
async fn apply_request_record_level_from_data<T>(data: &T, event: &mut UsageEvent)
where
T: UsageRuntimeAccess,
{
match data.request_record_level().await {
Ok(level) => apply_request_record_level(level, event),
Err(err) => {
warn!(
event_name = "usage_request_record_level_read_failed",
log_type = "event",
request_id = %event.request_id,
fallback = "full",
error = %err,
"usage runtime failed to read request record level; keeping full capture"
);
}
}
}
fn apply_request_record_level(level: UsageRequestRecordLevel, event: &mut UsageEvent) {
if !matches!(level, UsageRequestRecordLevel::Basic) {
return;
}
event.data.request_body = None;
event.data.request_body_ref = None;
event.data.provider_request_body = None;
event.data.provider_request_body_ref = None;
event.data.response_body = None;
event.data.response_body_ref = None;
event.data.client_response_body = None;
event.data.client_response_body_ref = None;
}
fn boxed_usage_task<F>(task: F) -> Pin<Box<dyn Future<Output = ()> + Send>>
where
F: Future<Output = ()> + Send + 'static,
@@ -444,3 +493,51 @@ fn now_unix_secs() -> u64 {
.unwrap_or_default()
.as_secs()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{apply_request_record_level, UsageRequestRecordLevel};
use crate::{UsageEvent, UsageEventData, UsageEventType};
#[test]
fn basic_request_record_level_strips_body_capture_but_preserves_derived_fields() {
let mut event = UsageEvent::new(
UsageEventType::Failed,
"req-basic-1",
UsageEventData {
provider_name: "OpenAI".to_string(),
model: "gpt-5".to_string(),
total_tokens: Some(42),
error_message: Some("upstream failed".to_string()),
request_body: Some(json!({"messages":[{"role":"user","content":"hello"}]})),
request_body_ref: Some("usage://request/req-basic-1/request_body".to_string()),
provider_request_body: Some(json!({"model":"gpt-5"})),
provider_request_body_ref: Some(
"usage://request/req-basic-1/provider_request_body".to_string(),
),
response_body: Some(json!({"error":{"message":"bad gateway"}})),
response_body_ref: Some("usage://request/req-basic-1/response_body".to_string()),
client_response_body: Some(json!({"detail":"bad gateway"})),
client_response_body_ref: Some(
"usage://request/req-basic-1/client_response_body".to_string(),
),
..UsageEventData::default()
},
);
apply_request_record_level(UsageRequestRecordLevel::Basic, &mut event);
assert_eq!(event.data.total_tokens, Some(42));
assert_eq!(event.data.error_message.as_deref(), Some("upstream failed"));
assert!(event.data.request_body.is_none());
assert!(event.data.request_body_ref.is_none());
assert!(event.data.provider_request_body.is_none());
assert!(event.data.provider_request_body_ref.is_none());
assert!(event.data.response_body.is_none());
assert!(event.data.response_body_ref.is_none());
assert!(event.data.client_response_body.is_none());
assert!(event.data.client_response_body_ref.is_none());
}
}