mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
Use header-only usage server timing
This commit is contained in:
@@ -9,9 +9,9 @@ use aether_admin::observability::usage::{
|
||||
admin_usage_data_unavailable_response, admin_usage_has_fallback, admin_usage_is_failed,
|
||||
admin_usage_matches_search, admin_usage_matches_username, admin_usage_parse_ids,
|
||||
admin_usage_parse_limit, admin_usage_parse_offset, admin_usage_provider_key_name,
|
||||
admin_usage_record_json, build_admin_usage_active_requests_response,
|
||||
build_admin_usage_records_response, build_admin_usage_summary_stats_response_from_summary,
|
||||
usage_server_now_unix_ms, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
admin_usage_record_json, attach_usage_server_now_header,
|
||||
build_admin_usage_active_requests_response, build_admin_usage_records_response,
|
||||
build_admin_usage_summary_stats_response_from_summary, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use aether_data::repository::users::StoredUserSummary;
|
||||
use aether_data_contracts::repository::{
|
||||
@@ -314,14 +314,15 @@ fn build_admin_usage_records_response_with_attempt_flags(
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(json!({
|
||||
"server_now_unix_ms": usage_server_now_unix_ms(),
|
||||
"records": records,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}))
|
||||
.into_response()
|
||||
attach_usage_server_now_header(
|
||||
Json(json!({
|
||||
"records": records,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}))
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
|
||||
fn build_admin_usage_records_query(
|
||||
|
||||
@@ -4,6 +4,7 @@ use aether_ai_serving::UPSTREAM_IS_STREAM_KEY;
|
||||
use aether_billing::{
|
||||
normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate,
|
||||
};
|
||||
use aether_contracts::USAGE_SERVER_NOW_UNIX_MS_HEADER;
|
||||
use aether_data_contracts::repository::usage::{
|
||||
StoredRequestUsageAudit, StoredUsageBreakdownSummaryRow, StoredUsageDailySummary,
|
||||
UsageAuditKeywordSearchQuery, UsageAuditListQuery, UsageBreakdownGroupBy,
|
||||
@@ -33,6 +34,17 @@ fn users_me_usage_server_now_unix_ms() -> u64 {
|
||||
u64::try_from(Utc::now().timestamp_millis()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn attach_users_me_usage_server_now_header(mut response: Response<Body>) -> Response<Body> {
|
||||
if let Ok(value) = http::HeaderValue::from_str(&users_me_usage_server_now_unix_ms().to_string())
|
||||
{
|
||||
response.headers_mut().insert(
|
||||
http::HeaderName::from_static(USAGE_SERVER_NOW_UNIX_MS_HEADER),
|
||||
value,
|
||||
);
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
fn build_users_me_usage_reader_unavailable_response() -> Response<Body> {
|
||||
build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
@@ -1075,7 +1087,6 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
.flatten();
|
||||
|
||||
let mut payload = json!({
|
||||
"server_now_unix_ms": users_me_usage_server_now_unix_ms(),
|
||||
"total_requests": total_requests,
|
||||
"total_input_tokens": total_input_tokens,
|
||||
"total_output_tokens": total_output_tokens,
|
||||
@@ -1099,7 +1110,7 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
&summary_by_provider
|
||||
));
|
||||
}
|
||||
Json(payload).into_response()
|
||||
attach_users_me_usage_server_now_header(Json(payload).into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn handle_users_me_usage_active_get(
|
||||
@@ -1174,14 +1185,15 @@ pub(super) async fn handle_users_me_usage_active_get(
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
"server_now_unix_ms": users_me_usage_server_now_unix_ms(),
|
||||
"requests": items
|
||||
.iter()
|
||||
.map(build_users_me_usage_active_payload)
|
||||
.collect::<Vec<_>>(),
|
||||
}))
|
||||
.into_response()
|
||||
attach_users_me_usage_server_now_header(
|
||||
Json(json!({
|
||||
"requests": items
|
||||
.iter()
|
||||
.map(build_users_me_usage_active_payload)
|
||||
.collect::<Vec<_>>(),
|
||||
}))
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn handle_users_me_usage_interval_timeline_get(
|
||||
@@ -1370,14 +1382,21 @@ async fn build_usage_heatmap_summaries(
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use aether_contracts::USAGE_SERVER_NOW_UNIX_MS_HEADER;
|
||||
use aether_data_contracts::repository::usage::StoredRequestUsageAudit;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
build_users_me_usage_active_payload, build_users_me_usage_record_payload,
|
||||
users_me_usage_client_is_stream, users_me_usage_is_failed,
|
||||
users_me_usage_server_now_unix_ms, users_me_usage_upstream_is_stream,
|
||||
attach_users_me_usage_server_now_header, build_users_me_usage_active_payload,
|
||||
build_users_me_usage_record_payload, users_me_usage_client_is_stream,
|
||||
users_me_usage_is_failed, users_me_usage_server_now_unix_ms,
|
||||
users_me_usage_upstream_is_stream,
|
||||
};
|
||||
|
||||
fn sample_usage(status: &str) -> StoredRequestUsageAudit {
|
||||
@@ -1433,6 +1452,28 @@ mod tests {
|
||||
assert!(value > 1_000_000_000_000);
|
||||
}
|
||||
|
||||
fn assert_users_me_usage_server_now_header(response: &Response<Body>) {
|
||||
let value = response
|
||||
.headers()
|
||||
.get(USAGE_SERVER_NOW_UNIX_MS_HEADER)
|
||||
.expect("user usage response should include server now header")
|
||||
.to_str()
|
||||
.expect("server now header should be valid ASCII")
|
||||
.parse::<u64>()
|
||||
.expect("server now header should be epoch millis");
|
||||
|
||||
assert!(value > 1_000_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn users_me_usage_server_now_header_is_added_to_response() {
|
||||
let response = attach_users_me_usage_server_now_header(
|
||||
Json(json!({ "requests": [] })).into_response(),
|
||||
);
|
||||
|
||||
assert_users_me_usage_server_now_header(&response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_usage_record_payload_rehydrates_cache_creation_total_from_classified_fields() {
|
||||
let item = StoredRequestUsageAudit {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use aether_contracts::USAGE_SERVER_NOW_UNIX_MS_HEADER;
|
||||
use axum::body::Body;
|
||||
use axum::extract::{Request, State};
|
||||
use axum::http::{self, HeaderValue, Response};
|
||||
@@ -6,6 +7,8 @@ use axum::middleware::Next;
|
||||
use crate::headers::header_value_str;
|
||||
use crate::state::{AppState, FrontdoorCorsConfig};
|
||||
|
||||
const FRONTDOOR_CREDENTIALS_EXPOSE_HEADERS: &str = "*, x-aether-server-now-unix-ms";
|
||||
|
||||
fn append_vary(headers: &mut http::HeaderMap, value: &'static str) {
|
||||
headers.append(http::header::VARY, HeaderValue::from_static(value));
|
||||
}
|
||||
@@ -31,7 +34,11 @@ fn apply_frontdoor_cors_headers(
|
||||
);
|
||||
headers.insert(
|
||||
http::header::ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||
HeaderValue::from_static("*"),
|
||||
HeaderValue::from_static(if cors.allow_credentials() {
|
||||
FRONTDOOR_CREDENTIALS_EXPOSE_HEADERS
|
||||
} else {
|
||||
"*"
|
||||
}),
|
||||
);
|
||||
if let Some(value) = requested_headers {
|
||||
if let Ok(value) = HeaderValue::from_str(value) {
|
||||
@@ -109,3 +116,52 @@ pub(crate) async fn frontdoor_cors_middleware(
|
||||
);
|
||||
response
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_exposes_header(value: &HeaderValue, expected: &str) {
|
||||
let exposed_headers = value
|
||||
.to_str()
|
||||
.expect("expose headers should be valid ASCII");
|
||||
assert!(
|
||||
exposed_headers
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.any(|header| header.eq_ignore_ascii_case(expected)),
|
||||
"{exposed_headers} should include {expected}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontdoor_cors_explicitly_exposes_usage_server_time_for_credentials() {
|
||||
let cors = FrontdoorCorsConfig::new(vec!["http://localhost:5173".to_string()], true)
|
||||
.expect("cors config should build");
|
||||
let mut headers = http::HeaderMap::new();
|
||||
|
||||
apply_frontdoor_cors_headers(&mut headers, &cors, "http://localhost:5173", None);
|
||||
|
||||
let expose_headers = headers
|
||||
.get(http::header::ACCESS_CONTROL_EXPOSE_HEADERS)
|
||||
.expect("expose headers should be set");
|
||||
assert_exposes_header(expose_headers, "*");
|
||||
assert_exposes_header(expose_headers, USAGE_SERVER_NOW_UNIX_MS_HEADER);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontdoor_cors_keeps_wildcard_expose_headers_without_credentials() {
|
||||
let cors = FrontdoorCorsConfig::new(vec!["http://localhost:5173".to_string()], false)
|
||||
.expect("cors config should build");
|
||||
let mut headers = http::HeaderMap::new();
|
||||
|
||||
apply_frontdoor_cors_headers(&mut headers, &cors, "http://localhost:5173", None);
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(http::header::ACCESS_CONTROL_EXPOSE_HEADERS)
|
||||
.expect("expose headers should be set"),
|
||||
"*"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::tests::{
|
||||
build_state_with_execution_runtime_override, json, start_server, AppState, Arc, Body,
|
||||
FrontdoorCorsConfig, Mutex, Request, Router, StatusCode, FRONTDOOR_MANIFEST_PATH, READYZ_PATH,
|
||||
};
|
||||
use aether_contracts::USAGE_SERVER_NOW_UNIX_MS_HEADER;
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data::repository::auth::InMemoryAuthApiKeySnapshotRepository;
|
||||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
||||
@@ -440,12 +441,19 @@ async fn gateway_adds_cors_headers_to_proxied_responses() {
|
||||
.expect("allow origin header"),
|
||||
"http://localhost:3000"
|
||||
);
|
||||
assert_eq!(
|
||||
response_headers
|
||||
.get("access-control-expose-headers")
|
||||
.expect("expose headers header"),
|
||||
"*"
|
||||
);
|
||||
let expose_headers = response_headers
|
||||
.get("access-control-expose-headers")
|
||||
.expect("expose headers header")
|
||||
.to_str()
|
||||
.expect("expose headers should be valid ASCII");
|
||||
assert!(expose_headers
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.any(|header| header == "*"));
|
||||
assert!(expose_headers
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.any(|header| header.eq_ignore_ascii_case(USAGE_SERVER_NOW_UNIX_MS_HEADER)));
|
||||
assert_eq!(
|
||||
*execution_runtime_hits.lock().expect("mutex should lock"),
|
||||
1
|
||||
|
||||
Reference in New Issue
Block a user