Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/aisix-obs/src/access_log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ pub struct AccessLog<'a> {
pub completion_tokens: Option<u64>,
pub total_tokens: Option<u64>,
pub request_id: &'a str,
pub served_by_model: Option<&'a str>,
pub routing_attempt_count: Option<u32>,
pub routing_fallback_count: Option<u32>,
pub routing_attempts: Option<&'a str>,
}

impl AccessLog<'_> {
Expand All @@ -40,6 +44,10 @@ impl AccessLog<'_> {
completion_tokens = self.completion_tokens,
total_tokens = self.total_tokens,
request_id = self.request_id,
served_by_model = self.served_by_model,
routing_attempt_count = self.routing_attempt_count,
routing_fallback_count = self.routing_fallback_count,
routing_attempts = self.routing_attempts,
"proxy request completed",
);
}
Expand Down Expand Up @@ -101,6 +109,10 @@ mod tests {
completion_tokens: Some(1),
total_tokens: Some(3),
request_id: "req-abc",
served_by_model: Some("fallback-target"),
routing_attempt_count: Some(2),
routing_fallback_count: Some(1),
routing_attempts: Some(r#"[{"model":"primary","success":false},{"model":"fallback-target","success":true}]"#),
}
.emit();
});
Expand All @@ -113,6 +125,13 @@ mod tests {
assert!(out.contains("provider=\"openai\"") || out.contains("provider=openai"));
assert!(out.contains("total_tokens=3"));
assert!(out.contains("request_id=\"req-abc\"") || out.contains("request_id=req-abc"));
assert!(
out.contains("served_by_model=\"fallback-target\"")
|| out.contains("served_by_model=fallback-target")
);
assert!(out.contains("routing_attempt_count=2"));
assert!(out.contains("routing_fallback_count=1"));
assert!(out.contains("routing_attempts="));
}

#[test]
Expand All @@ -138,6 +157,10 @@ mod tests {
completion_tokens: None,
total_tokens: None,
request_id: "req-xyz",
served_by_model: None,
routing_attempt_count: None,
routing_fallback_count: None,
routing_attempts: None,
}
.emit();
});
Expand Down
2 changes: 1 addition & 1 deletion crates/aisix-obs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub use metrics::{
};
pub use otlp::{install_otlp_tracer, shutdown_otlp, OtlpError, OtlpHandle};
pub use otlp_http_sink::OtlpHttpFanOut;
pub use usage::{UsageEvent, UsageSink};
pub use usage::{RoutingAttemptEvent, UsageEvent, UsageSink};

#[derive(Debug, thiserror::Error)]
pub enum ObsError {
Expand Down
74 changes: 74 additions & 0 deletions crates/aisix-obs/src/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,20 @@

use serde::Serialize;

/// One upstream attempt made while serving a routing-model request.
/// This intentionally carries only low-sensitivity operational fields:
/// target name, per-target attempt index, status/error class, and outcome.
#[derive(Debug, Clone, Default, Serialize)]
pub struct RoutingAttemptEvent {
pub model: String,
pub attempt: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status: Option<u16>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub error: String,
pub success: bool,
}

/// One usage event. Emitted at end-of-request (success / upstream error /
/// guardrail block) per chat completion. Field shape pinned to the
/// cp-api wire (snake_case via serde).
Expand Down Expand Up @@ -187,6 +201,26 @@ pub struct UsageEvent {
/// the wire = legacy DP image; cp-api stores empty as NULL.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub inbound_protocol: String,

/// Display name of the routing target that ultimately served the
/// request. Empty for direct-model requests, cache hits, and routing
/// requests where every candidate failed.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub served_by_model: String,

/// Number of upstream attempts made for a routing-model request.
/// Zero means routing did not run or no upstream attempt was made.
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub routing_attempt_count: u32,

/// Number of times routing moved from one target model to another.
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub routing_fallback_count: u32,

/// Per-attempt routing trace for debugging failover. Omitted for
/// direct-model requests and cache hits.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub routing_attempts: Vec<RoutingAttemptEvent>,
}

#[inline]
Expand Down Expand Up @@ -347,6 +381,46 @@ mod tests {
assert!(json.contains(r#""ttft_ms":123"#));
}

#[test]
fn routing_fields_serialise_only_when_present() {
let ev = UsageEvent {
request_id: "req-routing".into(),
served_by_model: "secondary".into(),
routing_attempt_count: 3,
routing_fallback_count: 1,
routing_attempts: vec![
RoutingAttemptEvent {
model: "primary".into(),
attempt: 1,
status: Some(502),
error: "upstream_status".into(),
success: false,
},
RoutingAttemptEvent {
model: "secondary".into(),
attempt: 1,
status: Some(200),
error: String::new(),
success: true,
},
],
..Default::default()
};
let json = serde_json::to_string(&ev).unwrap();
assert!(json.contains(r#""served_by_model":"secondary""#));
assert!(json.contains(r#""routing_attempt_count":3"#));
assert!(json.contains(r#""routing_fallback_count":1"#));
assert!(json.contains(r#""routing_attempts""#));
assert!(json.contains(r#""model":"primary""#));
assert!(json.contains(r#""error":"upstream_status""#));

let empty = serde_json::to_string(&UsageEvent::default()).unwrap();
assert!(!empty.contains("served_by_model"));
assert!(!empty.contains("routing_attempt_count"));
assert!(!empty.contains("routing_fallback_count"));
assert!(!empty.contains("routing_attempts"));
}

fn sample_event(id: &str) -> UsageEvent {
UsageEvent {
request_id: id.into(),
Expand Down
4 changes: 4 additions & 0 deletions crates/aisix-proxy/src/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,10 @@ fn emit_access_log(
completion_tokens: None,
total_tokens: None,
request_id,
served_by_model: None,
routing_attempt_count: None,
routing_fallback_count: None,
routing_attempts: None,
}
.emit();
}
Expand Down
Loading
Loading