mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
Merge remote-tracking branch 'origin/pr/384' into codex/pr-376-377-383-384-combined
# Conflicts: # frontend/src/views/admin/PerformanceAnalysis.vue
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use super::range::{build_comparison_range, parse_bounded_u32};
|
use super::range::{build_comparison_range, parse_bounded_u32};
|
||||||
use super::resolve_admin_usage_time_range;
|
use super::resolve_admin_usage_time_range;
|
||||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||||
use crate::handlers::admin::shared::query_param_value;
|
use crate::handlers::admin::shared::{query_param_optional_bool, query_param_value};
|
||||||
use crate::GatewayError;
|
use crate::GatewayError;
|
||||||
use aether_admin::observability::stats::{
|
use aether_admin::observability::stats::{
|
||||||
admin_stats_bad_request_response, admin_stats_comparison_empty_response,
|
admin_stats_bad_request_response, admin_stats_comparison_empty_response,
|
||||||
@@ -203,6 +203,14 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
|
|||||||
Ok(value) => value.unwrap_or(8) as usize,
|
Ok(value) => value.unwrap_or(8) as usize,
|
||||||
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
|
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
|
||||||
};
|
};
|
||||||
|
let slow_threshold_ms =
|
||||||
|
match query_param_value(request_context.query_string(), "slow_threshold_ms")
|
||||||
|
.map(|value| parse_bounded_u32("slow_threshold_ms", &value, 1, 600_000))
|
||||||
|
.transpose()
|
||||||
|
{
|
||||||
|
Ok(value) => u64::from(value.unwrap_or(10_000)),
|
||||||
|
Err(detail) => return Ok(Some(admin_stats_bad_request_response(detail))),
|
||||||
|
};
|
||||||
if !state.has_usage_data_reader() {
|
if !state.has_usage_data_reader() {
|
||||||
return Ok(Some(admin_stats_provider_performance_empty_response()));
|
return Ok(Some(admin_stats_provider_performance_empty_response()));
|
||||||
}
|
}
|
||||||
@@ -218,6 +226,16 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
|
|||||||
granularity,
|
granularity,
|
||||||
tz_offset_minutes: time_range.tz_offset_minutes,
|
tz_offset_minutes: time_range.tz_offset_minutes,
|
||||||
limit,
|
limit,
|
||||||
|
provider_id: query_param_value(request_context.query_string(), "provider_id"),
|
||||||
|
model: query_param_value(request_context.query_string(), "model"),
|
||||||
|
api_format: query_param_value(request_context.query_string(), "api_format"),
|
||||||
|
endpoint_kind: query_param_value(request_context.query_string(), "endpoint_kind"),
|
||||||
|
is_stream: query_param_optional_bool(request_context.query_string(), "is_stream"),
|
||||||
|
has_format_conversion: query_param_optional_bool(
|
||||||
|
request_context.query_string(),
|
||||||
|
"has_format_conversion",
|
||||||
|
),
|
||||||
|
slow_threshold_ms,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(Some(build_admin_stats_provider_performance_response(
|
return Ok(Some(build_admin_stats_provider_performance_response(
|
||||||
|
|||||||
@@ -960,6 +960,9 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
|||||||
assert_eq!(payload["summary"]["avg_output_tps"], 20.17);
|
assert_eq!(payload["summary"]["avg_output_tps"], 20.17);
|
||||||
assert_eq!(payload["summary"]["avg_first_byte_time_ms"], 55.0);
|
assert_eq!(payload["summary"]["avg_first_byte_time_ms"], 55.0);
|
||||||
assert_eq!(payload["summary"]["avg_response_time_ms"], 590.91);
|
assert_eq!(payload["summary"]["avg_response_time_ms"], 590.91);
|
||||||
|
assert_eq!(payload["summary"]["p99_response_time_ms"], 1000);
|
||||||
|
assert_eq!(payload["summary"]["response_time_sample_count"], 11);
|
||||||
|
assert_eq!(payload["summary"]["slow_request_count"], 0);
|
||||||
|
|
||||||
assert_eq!(payload["providers"].as_array().map(Vec::len), Some(2));
|
assert_eq!(payload["providers"].as_array().map(Vec::len), Some(2));
|
||||||
assert_eq!(payload["providers"][0]["provider_id"], "provider-1");
|
assert_eq!(payload["providers"][0]["provider_id"], "provider-1");
|
||||||
@@ -973,9 +976,13 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
|||||||
assert_eq!(payload["providers"][0]["avg_first_byte_time_ms"], 55.0);
|
assert_eq!(payload["providers"][0]["avg_first_byte_time_ms"], 55.0);
|
||||||
assert_eq!(payload["providers"][0]["avg_response_time_ms"], 550.0);
|
assert_eq!(payload["providers"][0]["avg_response_time_ms"], 550.0);
|
||||||
assert_eq!(payload["providers"][0]["p90_response_time_ms"], 910);
|
assert_eq!(payload["providers"][0]["p90_response_time_ms"], 910);
|
||||||
|
assert_eq!(payload["providers"][0]["p99_response_time_ms"], 991);
|
||||||
assert_eq!(payload["providers"][0]["p90_first_byte_time_ms"], 91);
|
assert_eq!(payload["providers"][0]["p90_first_byte_time_ms"], 91);
|
||||||
|
assert_eq!(payload["providers"][0]["p99_first_byte_time_ms"], 99);
|
||||||
assert_eq!(payload["providers"][0]["tps_sample_count"], 10);
|
assert_eq!(payload["providers"][0]["tps_sample_count"], 10);
|
||||||
|
assert_eq!(payload["providers"][0]["response_time_sample_count"], 10);
|
||||||
assert_eq!(payload["providers"][0]["first_byte_sample_count"], 10);
|
assert_eq!(payload["providers"][0]["first_byte_sample_count"], 10);
|
||||||
|
assert_eq!(payload["providers"][0]["slow_request_count"], 0);
|
||||||
|
|
||||||
assert_eq!(payload["providers"][1]["provider_id"], "provider-2");
|
assert_eq!(payload["providers"][1]["provider_id"], "provider-2");
|
||||||
assert_eq!(payload["providers"][1]["provider"], "Anthropic");
|
assert_eq!(payload["providers"][1]["provider"], "Anthropic");
|
||||||
@@ -994,6 +1001,7 @@ async fn gateway_handles_admin_stats_provider_performance_locally_with_trusted_a
|
|||||||
assert_eq!(payload["timeline"][0]["provider_id"], "provider-1");
|
assert_eq!(payload["timeline"][0]["provider_id"], "provider-1");
|
||||||
assert_eq!(payload["timeline"][0]["avg_output_tps"], 20.2);
|
assert_eq!(payload["timeline"][0]["avg_output_tps"], 20.2);
|
||||||
assert_eq!(payload["timeline"][0]["success_rate"], 90.91);
|
assert_eq!(payload["timeline"][0]["success_rate"], 90.91);
|
||||||
|
assert_eq!(payload["timeline"][0]["slow_request_count"], 0);
|
||||||
assert_eq!(payload["timeline"][1]["provider_id"], "provider-2");
|
assert_eq!(payload["timeline"][1]["provider_id"], "provider-2");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
payload["timeline"][1]["avg_first_byte_time_ms"],
|
payload["timeline"][1]["avg_first_byte_time_ms"],
|
||||||
|
|||||||
@@ -1110,9 +1110,13 @@ pub fn build_admin_stats_provider_performance_response(
|
|||||||
"avg_first_byte_time_ms": rounded_option(row.avg_first_byte_time_ms, 2),
|
"avg_first_byte_time_ms": rounded_option(row.avg_first_byte_time_ms, 2),
|
||||||
"avg_response_time_ms": rounded_option(row.avg_response_time_ms, 2),
|
"avg_response_time_ms": rounded_option(row.avg_response_time_ms, 2),
|
||||||
"p90_response_time_ms": row.p90_response_time_ms,
|
"p90_response_time_ms": row.p90_response_time_ms,
|
||||||
|
"p99_response_time_ms": row.p99_response_time_ms,
|
||||||
"p90_first_byte_time_ms": row.p90_first_byte_time_ms,
|
"p90_first_byte_time_ms": row.p90_first_byte_time_ms,
|
||||||
|
"p99_first_byte_time_ms": row.p99_first_byte_time_ms,
|
||||||
"tps_sample_count": row.tps_sample_count,
|
"tps_sample_count": row.tps_sample_count,
|
||||||
|
"response_time_sample_count": row.response_time_sample_count,
|
||||||
"first_byte_sample_count": row.first_byte_sample_count,
|
"first_byte_sample_count": row.first_byte_sample_count,
|
||||||
|
"slow_request_count": row.slow_request_count,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -1129,6 +1133,7 @@ pub fn build_admin_stats_provider_performance_response(
|
|||||||
"avg_output_tps": rounded_option(row.avg_output_tps, 2),
|
"avg_output_tps": rounded_option(row.avg_output_tps, 2),
|
||||||
"avg_first_byte_time_ms": rounded_option(row.avg_first_byte_time_ms, 2),
|
"avg_first_byte_time_ms": rounded_option(row.avg_first_byte_time_ms, 2),
|
||||||
"avg_response_time_ms": rounded_option(row.avg_response_time_ms, 2),
|
"avg_response_time_ms": rounded_option(row.avg_response_time_ms, 2),
|
||||||
|
"slow_request_count": row.slow_request_count,
|
||||||
"success_rate": success_rate(row.request_count, row.success_count),
|
"success_rate": success_rate(row.request_count, row.success_count),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1141,6 +1146,14 @@ pub fn build_admin_stats_provider_performance_response(
|
|||||||
"avg_output_tps": rounded_option(summary.avg_output_tps, 2),
|
"avg_output_tps": rounded_option(summary.avg_output_tps, 2),
|
||||||
"avg_first_byte_time_ms": rounded_option(summary.avg_first_byte_time_ms, 2),
|
"avg_first_byte_time_ms": rounded_option(summary.avg_first_byte_time_ms, 2),
|
||||||
"avg_response_time_ms": rounded_option(summary.avg_response_time_ms, 2),
|
"avg_response_time_ms": rounded_option(summary.avg_response_time_ms, 2),
|
||||||
|
"p90_response_time_ms": summary.p90_response_time_ms,
|
||||||
|
"p99_response_time_ms": summary.p99_response_time_ms,
|
||||||
|
"p90_first_byte_time_ms": summary.p90_first_byte_time_ms,
|
||||||
|
"p99_first_byte_time_ms": summary.p99_first_byte_time_ms,
|
||||||
|
"tps_sample_count": summary.tps_sample_count,
|
||||||
|
"response_time_sample_count": summary.response_time_sample_count,
|
||||||
|
"first_byte_sample_count": summary.first_byte_sample_count,
|
||||||
|
"slow_request_count": summary.slow_request_count,
|
||||||
},
|
},
|
||||||
"providers": providers,
|
"providers": providers,
|
||||||
"timeline": timeline,
|
"timeline": timeline,
|
||||||
|
|||||||
@@ -957,6 +957,13 @@ pub struct UsageProviderPerformanceQuery {
|
|||||||
pub granularity: UsageTimeSeriesGranularity,
|
pub granularity: UsageTimeSeriesGranularity,
|
||||||
pub tz_offset_minutes: i32,
|
pub tz_offset_minutes: i32,
|
||||||
pub limit: usize,
|
pub limit: usize,
|
||||||
|
pub provider_id: Option<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub api_format: Option<String>,
|
||||||
|
pub endpoint_kind: Option<String>,
|
||||||
|
pub is_stream: Option<bool>,
|
||||||
|
pub has_format_conversion: Option<bool>,
|
||||||
|
pub slow_threshold_ms: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -966,6 +973,14 @@ pub struct StoredUsageProviderPerformanceSummary {
|
|||||||
pub avg_output_tps: Option<f64>,
|
pub avg_output_tps: Option<f64>,
|
||||||
pub avg_first_byte_time_ms: Option<f64>,
|
pub avg_first_byte_time_ms: Option<f64>,
|
||||||
pub avg_response_time_ms: Option<f64>,
|
pub avg_response_time_ms: Option<f64>,
|
||||||
|
pub p90_response_time_ms: Option<u64>,
|
||||||
|
pub p99_response_time_ms: Option<u64>,
|
||||||
|
pub p90_first_byte_time_ms: Option<u64>,
|
||||||
|
pub p99_first_byte_time_ms: Option<u64>,
|
||||||
|
pub tps_sample_count: u64,
|
||||||
|
pub response_time_sample_count: u64,
|
||||||
|
pub first_byte_sample_count: u64,
|
||||||
|
pub slow_request_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -979,9 +994,13 @@ pub struct StoredUsageProviderPerformanceProviderRow {
|
|||||||
pub avg_first_byte_time_ms: Option<f64>,
|
pub avg_first_byte_time_ms: Option<f64>,
|
||||||
pub avg_response_time_ms: Option<f64>,
|
pub avg_response_time_ms: Option<f64>,
|
||||||
pub p90_response_time_ms: Option<u64>,
|
pub p90_response_time_ms: Option<u64>,
|
||||||
|
pub p99_response_time_ms: Option<u64>,
|
||||||
pub p90_first_byte_time_ms: Option<u64>,
|
pub p90_first_byte_time_ms: Option<u64>,
|
||||||
|
pub p99_first_byte_time_ms: Option<u64>,
|
||||||
pub tps_sample_count: u64,
|
pub tps_sample_count: u64,
|
||||||
|
pub response_time_sample_count: u64,
|
||||||
pub first_byte_sample_count: u64,
|
pub first_byte_sample_count: u64,
|
||||||
|
pub slow_request_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||||
@@ -995,6 +1014,7 @@ pub struct StoredUsageProviderPerformanceTimelineRow {
|
|||||||
pub avg_output_tps: Option<f64>,
|
pub avg_output_tps: Option<f64>,
|
||||||
pub avg_first_byte_time_ms: Option<f64>,
|
pub avg_first_byte_time_ms: Option<f64>,
|
||||||
pub avg_response_time_ms: Option<f64>,
|
pub avg_response_time_ms: Option<f64>,
|
||||||
|
pub slow_request_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||||
|
|||||||
@@ -618,6 +618,36 @@ fn usage_matches_provider_performance_query(
|
|||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if let Some(provider_id) = query.provider_id.as_deref() {
|
||||||
|
if item.provider_id.as_deref() != Some(provider_id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(model) = query.model.as_deref() {
|
||||||
|
if item.model != model {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(api_format) = query.api_format.as_deref() {
|
||||||
|
if item.api_format.as_deref() != Some(api_format) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(endpoint_kind) = query.endpoint_kind.as_deref() {
|
||||||
|
if item.endpoint_kind.as_deref() != Some(endpoint_kind) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(is_stream) = query.is_stream {
|
||||||
|
if item.is_stream != is_stream {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(has_format_conversion) = query.has_format_conversion {
|
||||||
|
if item.has_format_conversion != has_format_conversion {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
usage_provider_performance_identity(item)
|
usage_provider_performance_identity(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1729,12 +1759,19 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
|||||||
response_time_sample_count: u64,
|
response_time_sample_count: u64,
|
||||||
response_times: Vec<u64>,
|
response_times: Vec<u64>,
|
||||||
first_byte_times: Vec<u64>,
|
first_byte_times: Vec<u64>,
|
||||||
|
slow_request_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProviderPerformanceBucket {
|
impl ProviderPerformanceBucket {
|
||||||
fn add(&mut self, item: &StoredRequestUsageAudit) {
|
fn add(&mut self, item: &StoredRequestUsageAudit, slow_threshold_ms: u64) {
|
||||||
self.request_count = self.request_count.saturating_add(1);
|
self.request_count = self.request_count.saturating_add(1);
|
||||||
self.output_tokens = self.output_tokens.saturating_add(item.output_tokens);
|
self.output_tokens = self.output_tokens.saturating_add(item.output_tokens);
|
||||||
|
if item
|
||||||
|
.response_time_ms
|
||||||
|
.is_some_and(|value| value >= slow_threshold_ms)
|
||||||
|
{
|
||||||
|
self.slow_request_count = self.slow_request_count.saturating_add(1);
|
||||||
|
}
|
||||||
if !usage_is_success(item) {
|
if !usage_is_success(item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1799,14 +1836,16 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
|||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
summary_bucket.add(item);
|
summary_bucket.add(item, query.slow_threshold_ms);
|
||||||
let bucket = grouped.entry(provider_id).or_default();
|
let bucket = grouped.entry(provider_id).or_default();
|
||||||
if bucket.provider.is_empty() {
|
if bucket.provider.is_empty() {
|
||||||
bucket.provider = provider;
|
bucket.provider = provider;
|
||||||
}
|
}
|
||||||
bucket.add(item);
|
bucket.add(item, query.slow_threshold_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut summary_response_times = summary_bucket.response_times.clone();
|
||||||
|
let mut summary_first_byte_times = summary_bucket.first_byte_times.clone();
|
||||||
let summary = StoredUsageProviderPerformanceSummary {
|
let summary = StoredUsageProviderPerformanceSummary {
|
||||||
request_count: summary_bucket.request_count,
|
request_count: summary_bucket.request_count,
|
||||||
success_count: summary_bucket.success_count,
|
success_count: summary_bucket.success_count,
|
||||||
@@ -1822,14 +1861,25 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
|||||||
summary_bucket.response_time_ms_sum,
|
summary_bucket.response_time_ms_sum,
|
||||||
summary_bucket.response_time_sample_count,
|
summary_bucket.response_time_sample_count,
|
||||||
),
|
),
|
||||||
|
p90_response_time_ms: usage_percentile_cont(&mut summary_response_times, 0.9),
|
||||||
|
p99_response_time_ms: usage_percentile_cont(&mut summary_response_times, 0.99),
|
||||||
|
p90_first_byte_time_ms: usage_percentile_cont(&mut summary_first_byte_times, 0.9),
|
||||||
|
p99_first_byte_time_ms: usage_percentile_cont(&mut summary_first_byte_times, 0.99),
|
||||||
|
tps_sample_count: summary_bucket.tps_sample_count,
|
||||||
|
response_time_sample_count: summary_bucket.response_time_sample_count,
|
||||||
|
first_byte_sample_count: summary_bucket.first_byte_sample_count,
|
||||||
|
slow_request_count: summary_bucket.slow_request_count,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut providers = grouped
|
let mut providers = grouped
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(provider_id, mut bucket)| {
|
.map(|(provider_id, mut bucket)| {
|
||||||
let p90_response_time_ms = usage_percentile_cont(&mut bucket.response_times, 0.9);
|
let p90_response_time_ms = usage_percentile_cont(&mut bucket.response_times, 0.9);
|
||||||
|
let p99_response_time_ms = usage_percentile_cont(&mut bucket.response_times, 0.99);
|
||||||
let p90_first_byte_time_ms =
|
let p90_first_byte_time_ms =
|
||||||
usage_percentile_cont(&mut bucket.first_byte_times, 0.9);
|
usage_percentile_cont(&mut bucket.first_byte_times, 0.9);
|
||||||
|
let p99_first_byte_time_ms =
|
||||||
|
usage_percentile_cont(&mut bucket.first_byte_times, 0.99);
|
||||||
StoredUsageProviderPerformanceProviderRow {
|
StoredUsageProviderPerformanceProviderRow {
|
||||||
provider_id,
|
provider_id,
|
||||||
provider: bucket.provider,
|
provider: bucket.provider,
|
||||||
@@ -1849,9 +1899,13 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
|||||||
bucket.response_time_sample_count,
|
bucket.response_time_sample_count,
|
||||||
),
|
),
|
||||||
p90_response_time_ms,
|
p90_response_time_ms,
|
||||||
|
p99_response_time_ms,
|
||||||
p90_first_byte_time_ms,
|
p90_first_byte_time_ms,
|
||||||
|
p99_first_byte_time_ms,
|
||||||
tps_sample_count: bucket.tps_sample_count,
|
tps_sample_count: bucket.tps_sample_count,
|
||||||
|
response_time_sample_count: bucket.response_time_sample_count,
|
||||||
first_byte_sample_count: bucket.first_byte_sample_count,
|
first_byte_sample_count: bucket.first_byte_sample_count,
|
||||||
|
slow_request_count: bucket.slow_request_count,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -1888,7 +1942,7 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
|||||||
if bucket.provider.is_empty() {
|
if bucket.provider.is_empty() {
|
||||||
bucket.provider = provider;
|
bucket.provider = provider;
|
||||||
}
|
}
|
||||||
bucket.add(item);
|
bucket.add(item, query.slow_threshold_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeline = timeline_grouped
|
let timeline = timeline_grouped
|
||||||
@@ -1913,6 +1967,7 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
|||||||
bucket.response_time_ms_sum,
|
bucket.response_time_ms_sum,
|
||||||
bucket.response_time_sample_count,
|
bucket.response_time_sample_count,
|
||||||
),
|
),
|
||||||
|
slow_request_count: bucket.slow_request_count,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -4875,6 +4930,13 @@ mod tests {
|
|||||||
granularity: UsageTimeSeriesGranularity::Hour,
|
granularity: UsageTimeSeriesGranularity::Hour,
|
||||||
tz_offset_minutes: 0,
|
tz_offset_minutes: 0,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
provider_id: None,
|
||||||
|
model: None,
|
||||||
|
api_format: None,
|
||||||
|
endpoint_kind: None,
|
||||||
|
is_stream: None,
|
||||||
|
has_format_conversion: None,
|
||||||
|
slow_threshold_ms: 10_000,
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("provider performance should summarize");
|
.expect("provider performance should summarize");
|
||||||
|
|||||||
@@ -1045,6 +1045,38 @@ fn decode_usage_provider_performance_summary(
|
|||||||
avg_response_time_ms: row
|
avg_response_time_ms: row
|
||||||
.try_get::<Option<f64>, _>("avg_response_time_ms")
|
.try_get::<Option<f64>, _>("avg_response_time_ms")
|
||||||
.map_postgres_err()?,
|
.map_postgres_err()?,
|
||||||
|
p90_response_time_ms: row
|
||||||
|
.try_get::<Option<i64>, _>("p90_response_time_ms")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.map(|value| value.max(0) as u64),
|
||||||
|
p99_response_time_ms: row
|
||||||
|
.try_get::<Option<i64>, _>("p99_response_time_ms")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.map(|value| value.max(0) as u64),
|
||||||
|
p90_first_byte_time_ms: row
|
||||||
|
.try_get::<Option<i64>, _>("p90_first_byte_time_ms")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.map(|value| value.max(0) as u64),
|
||||||
|
p99_first_byte_time_ms: row
|
||||||
|
.try_get::<Option<i64>, _>("p99_first_byte_time_ms")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.map(|value| value.max(0) as u64),
|
||||||
|
tps_sample_count: row
|
||||||
|
.try_get::<i64, _>("tps_sample_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
response_time_sample_count: row
|
||||||
|
.try_get::<i64, _>("response_time_sample_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
first_byte_sample_count: row
|
||||||
|
.try_get::<i64, _>("first_byte_sample_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
|
slow_request_count: row
|
||||||
|
.try_get::<i64, _>("slow_request_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,18 +1111,34 @@ fn decode_usage_provider_performance_provider_row(
|
|||||||
.try_get::<Option<i64>, _>("p90_response_time_ms")
|
.try_get::<Option<i64>, _>("p90_response_time_ms")
|
||||||
.map_postgres_err()?
|
.map_postgres_err()?
|
||||||
.map(|value| value.max(0) as u64),
|
.map(|value| value.max(0) as u64),
|
||||||
|
p99_response_time_ms: row
|
||||||
|
.try_get::<Option<i64>, _>("p99_response_time_ms")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.map(|value| value.max(0) as u64),
|
||||||
p90_first_byte_time_ms: row
|
p90_first_byte_time_ms: row
|
||||||
.try_get::<Option<i64>, _>("p90_first_byte_time_ms")
|
.try_get::<Option<i64>, _>("p90_first_byte_time_ms")
|
||||||
.map_postgres_err()?
|
.map_postgres_err()?
|
||||||
.map(|value| value.max(0) as u64),
|
.map(|value| value.max(0) as u64),
|
||||||
|
p99_first_byte_time_ms: row
|
||||||
|
.try_get::<Option<i64>, _>("p99_first_byte_time_ms")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.map(|value| value.max(0) as u64),
|
||||||
tps_sample_count: row
|
tps_sample_count: row
|
||||||
.try_get::<i64, _>("tps_sample_count")
|
.try_get::<i64, _>("tps_sample_count")
|
||||||
.map_postgres_err()?
|
.map_postgres_err()?
|
||||||
.max(0) as u64,
|
.max(0) as u64,
|
||||||
|
response_time_sample_count: row
|
||||||
|
.try_get::<i64, _>("response_time_sample_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
first_byte_sample_count: row
|
first_byte_sample_count: row
|
||||||
.try_get::<i64, _>("first_byte_sample_count")
|
.try_get::<i64, _>("first_byte_sample_count")
|
||||||
.map_postgres_err()?
|
.map_postgres_err()?
|
||||||
.max(0) as u64,
|
.max(0) as u64,
|
||||||
|
slow_request_count: row
|
||||||
|
.try_get::<i64, _>("slow_request_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,9 +1170,64 @@ fn decode_usage_provider_performance_timeline_row(
|
|||||||
avg_response_time_ms: row
|
avg_response_time_ms: row
|
||||||
.try_get::<Option<f64>, _>("avg_response_time_ms")
|
.try_get::<Option<f64>, _>("avg_response_time_ms")
|
||||||
.map_postgres_err()?,
|
.map_postgres_err()?,
|
||||||
|
slow_request_count: row
|
||||||
|
.try_get::<i64, _>("slow_request_count")
|
||||||
|
.map_postgres_err()?
|
||||||
|
.max(0) as u64,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_usage_provider_performance_text_filter(
|
||||||
|
builder: &mut QueryBuilder<'_, Postgres>,
|
||||||
|
column: &'static str,
|
||||||
|
value: &Option<String>,
|
||||||
|
) {
|
||||||
|
let Some(value) = value
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
builder
|
||||||
|
.push(" AND NULLIF(BTRIM(COALESCE(")
|
||||||
|
.push(column)
|
||||||
|
.push(", '')), '') = ")
|
||||||
|
.push_bind(value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_usage_provider_performance_filters(
|
||||||
|
builder: &mut QueryBuilder<'_, Postgres>,
|
||||||
|
query: &UsageProviderPerformanceQuery,
|
||||||
|
) {
|
||||||
|
push_usage_provider_performance_text_filter(
|
||||||
|
builder,
|
||||||
|
r#""usage".provider_id"#,
|
||||||
|
&query.provider_id,
|
||||||
|
);
|
||||||
|
push_usage_provider_performance_text_filter(builder, r#""usage".model"#, &query.model);
|
||||||
|
push_usage_provider_performance_text_filter(
|
||||||
|
builder,
|
||||||
|
r#""usage".api_format"#,
|
||||||
|
&query.api_format,
|
||||||
|
);
|
||||||
|
push_usage_provider_performance_text_filter(
|
||||||
|
builder,
|
||||||
|
r#""usage".endpoint_kind"#,
|
||||||
|
&query.endpoint_kind,
|
||||||
|
);
|
||||||
|
if let Some(is_stream) = query.is_stream {
|
||||||
|
builder
|
||||||
|
.push(r#" AND "usage".is_stream = "#)
|
||||||
|
.push_bind(is_stream);
|
||||||
|
}
|
||||||
|
if let Some(has_format_conversion) = query.has_format_conversion {
|
||||||
|
builder
|
||||||
|
.push(r#" AND "usage".has_format_conversion = "#)
|
||||||
|
.push_bind(has_format_conversion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_usage_time_series_bucket_row(
|
fn decode_usage_time_series_bucket_row(
|
||||||
row: &PgRow,
|
row: &PgRow,
|
||||||
) -> Result<StoredUsageTimeSeriesBucket, DataLayerError> {
|
) -> Result<StoredUsageTimeSeriesBucket, DataLayerError> {
|
||||||
@@ -4886,6 +4989,11 @@ WITH filtered_usage AS (
|
|||||||
AND NULLIF(BTRIM(COALESCE("usage".provider_id, '')), '') IS NOT NULL
|
AND NULLIF(BTRIM(COALESCE("usage".provider_id, '')), '') IS NOT NULL
|
||||||
AND lower(BTRIM(COALESCE("usage".provider_id, ''))) NOT IN ('unknown', 'pending')
|
AND lower(BTRIM(COALESCE("usage".provider_id, ''))) NOT IN ('unknown', 'pending')
|
||||||
AND lower(BTRIM(COALESCE("usage".provider_name, ''))) NOT IN ('unknown', 'pending')
|
AND lower(BTRIM(COALESCE("usage".provider_name, ''))) NOT IN ('unknown', 'pending')
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
push_usage_provider_performance_filters(&mut builder, query);
|
||||||
|
builder.push(
|
||||||
|
r#"
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*)::BIGINT AS request_count,
|
COUNT(*)::BIGINT AS request_count,
|
||||||
@@ -4910,7 +5018,49 @@ SELECT
|
|||||||
AVG(first_byte_time_ms::DOUBLE PRECISION)
|
AVG(first_byte_time_ms::DOUBLE PRECISION)
|
||||||
FILTER (WHERE success_flag = 1 AND has_first_byte_time) AS avg_first_byte_time_ms,
|
FILTER (WHERE success_flag = 1 AND has_first_byte_time) AS avg_first_byte_time_ms,
|
||||||
AVG(response_time_ms::DOUBLE PRECISION)
|
AVG(response_time_ms::DOUBLE PRECISION)
|
||||||
FILTER (WHERE success_flag = 1 AND has_response_time) AS avg_response_time_ms
|
FILTER (WHERE success_flag = 1 AND has_response_time) AS avg_response_time_ms,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(response_time_ms) FILTER (WHERE success_flag = 1 AND has_response_time) >= 10
|
||||||
|
THEN FLOOR(PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY response_time_ms)
|
||||||
|
FILTER (WHERE success_flag = 1 AND has_response_time))::BIGINT
|
||||||
|
ELSE NULL
|
||||||
|
END AS p90_response_time_ms,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(response_time_ms) FILTER (WHERE success_flag = 1 AND has_response_time) >= 10
|
||||||
|
THEN FLOOR(PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY response_time_ms)
|
||||||
|
FILTER (WHERE success_flag = 1 AND has_response_time))::BIGINT
|
||||||
|
ELSE NULL
|
||||||
|
END AS p99_response_time_ms,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time) >= 10
|
||||||
|
THEN FLOOR(PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY first_byte_time_ms)
|
||||||
|
FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
||||||
|
ELSE NULL
|
||||||
|
END AS p90_first_byte_time_ms,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time) >= 10
|
||||||
|
THEN FLOOR(PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY first_byte_time_ms)
|
||||||
|
FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
||||||
|
ELSE NULL
|
||||||
|
END AS p99_first_byte_time_ms,
|
||||||
|
COALESCE(SUM(CASE
|
||||||
|
WHEN success_flag = 1 AND response_time_ms > 0 AND output_tokens > 0
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END), 0)::BIGINT AS tps_sample_count,
|
||||||
|
(COUNT(response_time_ms) FILTER (WHERE success_flag = 1 AND has_response_time))::BIGINT
|
||||||
|
AS response_time_sample_count,
|
||||||
|
(COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
||||||
|
AS first_byte_sample_count,
|
||||||
|
COALESCE(SUM(CASE
|
||||||
|
WHEN has_response_time AND response_time_ms >= "#,
|
||||||
|
);
|
||||||
|
builder.push_bind(query.slow_threshold_ms as i64);
|
||||||
|
builder.push(
|
||||||
|
r#"
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END), 0)::BIGINT AS slow_request_count
|
||||||
FROM filtered_usage
|
FROM filtered_usage
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
@@ -4970,6 +5120,11 @@ WITH filtered_usage AS (
|
|||||||
AND NULLIF(BTRIM(COALESCE("usage".provider_id, '')), '') IS NOT NULL
|
AND NULLIF(BTRIM(COALESCE("usage".provider_id, '')), '') IS NOT NULL
|
||||||
AND lower(BTRIM(COALESCE("usage".provider_id, ''))) NOT IN ('unknown', 'pending')
|
AND lower(BTRIM(COALESCE("usage".provider_id, ''))) NOT IN ('unknown', 'pending')
|
||||||
AND lower(BTRIM(COALESCE("usage".provider_name, ''))) NOT IN ('unknown', 'pending')
|
AND lower(BTRIM(COALESCE("usage".provider_name, ''))) NOT IN ('unknown', 'pending')
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
push_usage_provider_performance_filters(&mut builder, query);
|
||||||
|
builder.push(
|
||||||
|
r#"
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
provider_id,
|
provider_id,
|
||||||
@@ -5004,19 +5159,42 @@ SELECT
|
|||||||
FILTER (WHERE success_flag = 1 AND has_response_time))::BIGINT
|
FILTER (WHERE success_flag = 1 AND has_response_time))::BIGINT
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END AS p90_response_time_ms,
|
END AS p90_response_time_ms,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(response_time_ms) FILTER (WHERE success_flag = 1 AND has_response_time) >= 10
|
||||||
|
THEN FLOOR(PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY response_time_ms)
|
||||||
|
FILTER (WHERE success_flag = 1 AND has_response_time))::BIGINT
|
||||||
|
ELSE NULL
|
||||||
|
END AS p99_response_time_ms,
|
||||||
CASE
|
CASE
|
||||||
WHEN COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time) >= 10
|
WHEN COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time) >= 10
|
||||||
THEN FLOOR(PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY first_byte_time_ms)
|
THEN FLOOR(PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY first_byte_time_ms)
|
||||||
FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
||||||
ELSE NULL
|
ELSE NULL
|
||||||
END AS p90_first_byte_time_ms,
|
END AS p90_first_byte_time_ms,
|
||||||
|
CASE
|
||||||
|
WHEN COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time) >= 10
|
||||||
|
THEN FLOOR(PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY first_byte_time_ms)
|
||||||
|
FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
||||||
|
ELSE NULL
|
||||||
|
END AS p99_first_byte_time_ms,
|
||||||
COALESCE(SUM(CASE
|
COALESCE(SUM(CASE
|
||||||
WHEN success_flag = 1 AND response_time_ms > 0 AND output_tokens > 0
|
WHEN success_flag = 1 AND response_time_ms > 0 AND output_tokens > 0
|
||||||
THEN 1
|
THEN 1
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END), 0)::BIGINT AS tps_sample_count,
|
END), 0)::BIGINT AS tps_sample_count,
|
||||||
|
(COUNT(response_time_ms) FILTER (WHERE success_flag = 1 AND has_response_time))::BIGINT
|
||||||
|
AS response_time_sample_count,
|
||||||
(COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
(COUNT(first_byte_time_ms) FILTER (WHERE success_flag = 1 AND has_first_byte_time))::BIGINT
|
||||||
AS first_byte_sample_count
|
AS first_byte_sample_count,
|
||||||
|
COALESCE(SUM(CASE
|
||||||
|
WHEN has_response_time AND response_time_ms >= "#,
|
||||||
|
);
|
||||||
|
builder.push_bind(query.slow_threshold_ms as i64);
|
||||||
|
builder.push(
|
||||||
|
r#"
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END), 0)::BIGINT AS slow_request_count
|
||||||
FROM filtered_usage
|
FROM filtered_usage
|
||||||
GROUP BY provider_id
|
GROUP BY provider_id
|
||||||
ORDER BY request_count DESC, provider_id ASC
|
ORDER BY request_count DESC, provider_id ASC
|
||||||
@@ -5096,6 +5274,11 @@ ORDER BY request_count DESC, provider_id ASC
|
|||||||
AND NULLIF(BTRIM(COALESCE("usage".provider_id, '')), '') IS NOT NULL
|
AND NULLIF(BTRIM(COALESCE("usage".provider_id, '')), '') IS NOT NULL
|
||||||
AND lower(BTRIM(COALESCE("usage".provider_id, ''))) NOT IN ('unknown', 'pending')
|
AND lower(BTRIM(COALESCE("usage".provider_id, ''))) NOT IN ('unknown', 'pending')
|
||||||
AND lower(BTRIM(COALESCE("usage".provider_name, ''))) NOT IN ('unknown', 'pending')
|
AND lower(BTRIM(COALESCE("usage".provider_name, ''))) NOT IN ('unknown', 'pending')
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
push_usage_provider_performance_filters(&mut builder, query);
|
||||||
|
builder.push(
|
||||||
|
r#"
|
||||||
AND "usage".provider_id = ANY("#,
|
AND "usage".provider_id = ANY("#,
|
||||||
);
|
);
|
||||||
builder.push_bind(provider_ids.to_vec());
|
builder.push_bind(provider_ids.to_vec());
|
||||||
@@ -5129,7 +5312,16 @@ SELECT
|
|||||||
AVG(first_byte_time_ms::DOUBLE PRECISION)
|
AVG(first_byte_time_ms::DOUBLE PRECISION)
|
||||||
FILTER (WHERE success_flag = 1 AND has_first_byte_time) AS avg_first_byte_time_ms,
|
FILTER (WHERE success_flag = 1 AND has_first_byte_time) AS avg_first_byte_time_ms,
|
||||||
AVG(response_time_ms::DOUBLE PRECISION)
|
AVG(response_time_ms::DOUBLE PRECISION)
|
||||||
FILTER (WHERE success_flag = 1 AND has_response_time) AS avg_response_time_ms
|
FILTER (WHERE success_flag = 1 AND has_response_time) AS avg_response_time_ms,
|
||||||
|
COALESCE(SUM(CASE
|
||||||
|
WHEN has_response_time AND response_time_ms >= "#,
|
||||||
|
);
|
||||||
|
builder.push_bind(query.slow_threshold_ms as i64);
|
||||||
|
builder.push(
|
||||||
|
r#"
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END), 0)::BIGINT AS slow_request_count
|
||||||
FROM filtered_usage
|
FROM filtered_usage
|
||||||
GROUP BY date, provider_id
|
GROUP BY date, provider_id
|
||||||
ORDER BY date ASC, provider_id ASC
|
ORDER BY date ASC, provider_id ASC
|
||||||
|
|||||||
@@ -453,6 +453,14 @@ export interface ProviderPerformanceSummary {
|
|||||||
avg_output_tps: number | null
|
avg_output_tps: number | null
|
||||||
avg_first_byte_time_ms: number | null
|
avg_first_byte_time_ms: number | null
|
||||||
avg_response_time_ms: number | null
|
avg_response_time_ms: number | null
|
||||||
|
p90_response_time_ms?: number | null
|
||||||
|
p99_response_time_ms?: number | null
|
||||||
|
p90_first_byte_time_ms?: number | null
|
||||||
|
p99_first_byte_time_ms?: number | null
|
||||||
|
tps_sample_count?: number
|
||||||
|
response_time_sample_count?: number
|
||||||
|
first_byte_sample_count?: number
|
||||||
|
slow_request_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderPerformanceItem {
|
export interface ProviderPerformanceItem {
|
||||||
@@ -467,9 +475,13 @@ export interface ProviderPerformanceItem {
|
|||||||
avg_first_byte_time_ms: number | null
|
avg_first_byte_time_ms: number | null
|
||||||
avg_response_time_ms: number | null
|
avg_response_time_ms: number | null
|
||||||
p90_response_time_ms: number | null
|
p90_response_time_ms: number | null
|
||||||
|
p99_response_time_ms?: number | null
|
||||||
p90_first_byte_time_ms: number | null
|
p90_first_byte_time_ms: number | null
|
||||||
|
p99_first_byte_time_ms?: number | null
|
||||||
tps_sample_count: number
|
tps_sample_count: number
|
||||||
|
response_time_sample_count?: number
|
||||||
first_byte_sample_count: number
|
first_byte_sample_count: number
|
||||||
|
slow_request_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderPerformanceTimelineItem {
|
export interface ProviderPerformanceTimelineItem {
|
||||||
@@ -482,6 +494,7 @@ export interface ProviderPerformanceTimelineItem {
|
|||||||
avg_first_byte_time_ms: number | null
|
avg_first_byte_time_ms: number | null
|
||||||
avg_response_time_ms: number | null
|
avg_response_time_ms: number | null
|
||||||
success_rate: number
|
success_rate: number
|
||||||
|
slow_request_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderPerformanceResponse {
|
export interface ProviderPerformanceResponse {
|
||||||
@@ -983,6 +996,13 @@ export const adminApi = {
|
|||||||
tz_offset_minutes?: number
|
tz_offset_minutes?: number
|
||||||
granularity?: 'day' | 'hour'
|
granularity?: 'day' | 'hour'
|
||||||
limit?: number
|
limit?: number
|
||||||
|
provider_id?: string
|
||||||
|
model?: string
|
||||||
|
api_format?: string
|
||||||
|
endpoint_kind?: string
|
||||||
|
is_stream?: boolean
|
||||||
|
has_format_conversion?: boolean
|
||||||
|
slow_threshold_ms?: number
|
||||||
}): Promise<ProviderPerformanceResponse> {
|
}): Promise<ProviderPerformanceResponse> {
|
||||||
const cacheKey = buildCacheKey('admin:stats:performance:providers', params)
|
const cacheKey = buildCacheKey('admin:stats:performance:providers', params)
|
||||||
return cachedRequest(
|
return cachedRequest(
|
||||||
|
|||||||
@@ -118,8 +118,21 @@ export interface GatewayMetricsSummary {
|
|||||||
distributed: GatewayGateMetrics
|
distributed: GatewayGateMetrics
|
||||||
tunnel: {
|
tunnel: {
|
||||||
proxyConnections: number | null
|
proxyConnections: number | null
|
||||||
|
availableProxyConnections: number | null
|
||||||
|
closingProxyConnections: number | null
|
||||||
|
drainingProxyConnections: number | null
|
||||||
|
softAvoidProxyConnections: number | null
|
||||||
nodes: number | null
|
nodes: number | null
|
||||||
activeStreams: number | null
|
activeStreams: number | null
|
||||||
|
outboundQueueDepthTotal: number | null
|
||||||
|
outboundQueueDepthMax: number | null
|
||||||
|
outboundQueueCapacityTotal: number | null
|
||||||
|
outboundQueueRejectedFullTotal: number | null
|
||||||
|
outboundQueueRejectedClosedTotal: number | null
|
||||||
|
proxyConnectionCongestedTotal: number | null
|
||||||
|
softAvoidSelectionTotal: number | null
|
||||||
|
selectionRetryTotal: number | null
|
||||||
|
selectionUnavailableTotal: number | null
|
||||||
}
|
}
|
||||||
fallbackTotal: number
|
fallbackTotal: number
|
||||||
fallbacks: GatewayFallbackMetricSummary[]
|
fallbacks: GatewayFallbackMetricSummary[]
|
||||||
@@ -159,8 +172,21 @@ export function buildGatewayMetricsSummary(text: string): GatewayMetricsSummary
|
|||||||
distributed: buildGateMetrics(samples, 'gateway_requests_distributed'),
|
distributed: buildGateMetrics(samples, 'gateway_requests_distributed'),
|
||||||
tunnel: {
|
tunnel: {
|
||||||
proxyConnections: findMetricValueNumber(samples, 'tunnel_proxy_connections'),
|
proxyConnections: findMetricValueNumber(samples, 'tunnel_proxy_connections'),
|
||||||
|
availableProxyConnections: findMetricValueNumber(samples, 'tunnel_proxy_connections_available'),
|
||||||
|
closingProxyConnections: findMetricValueNumber(samples, 'tunnel_proxy_connections_closing'),
|
||||||
|
drainingProxyConnections: findMetricValueNumber(samples, 'tunnel_proxy_connections_draining'),
|
||||||
|
softAvoidProxyConnections: findMetricValueNumber(samples, 'tunnel_proxy_connections_soft_avoid'),
|
||||||
nodes: findMetricValueNumber(samples, 'tunnel_nodes'),
|
nodes: findMetricValueNumber(samples, 'tunnel_nodes'),
|
||||||
activeStreams: findMetricValueNumber(samples, 'tunnel_active_streams'),
|
activeStreams: findMetricValueNumber(samples, 'tunnel_active_streams'),
|
||||||
|
outboundQueueDepthTotal: findMetricValueNumber(samples, 'tunnel_proxy_outbound_queue_depth_total'),
|
||||||
|
outboundQueueDepthMax: findMetricValueNumber(samples, 'tunnel_proxy_outbound_queue_depth_max'),
|
||||||
|
outboundQueueCapacityTotal: findMetricValueNumber(samples, 'tunnel_proxy_outbound_queue_capacity_total'),
|
||||||
|
outboundQueueRejectedFullTotal: findMetricValueNumber(samples, 'tunnel_proxy_outbound_queue_rejected_full_total'),
|
||||||
|
outboundQueueRejectedClosedTotal: findMetricValueNumber(samples, 'tunnel_proxy_outbound_queue_rejected_closed_total'),
|
||||||
|
proxyConnectionCongestedTotal: findMetricValueNumber(samples, 'tunnel_proxy_connection_congested_total'),
|
||||||
|
softAvoidSelectionTotal: findMetricValueNumber(samples, 'tunnel_proxy_soft_avoid_selection_total'),
|
||||||
|
selectionRetryTotal: findMetricValueNumber(samples, 'tunnel_proxy_selection_retry_total'),
|
||||||
|
selectionUnavailableTotal: findMetricValueNumber(samples, 'tunnel_proxy_selection_unavailable_total'),
|
||||||
},
|
},
|
||||||
fallbackTotal: fallbacks.reduce((total, item) => total + item.total, 0),
|
fallbackTotal: fallbacks.reduce((total, item) => total + item.total, 0),
|
||||||
fallbacks,
|
fallbacks,
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
title="刷新实时与历史性能数据"
|
title="刷新实时与历史性能数据"
|
||||||
@click="handleManualRefresh"
|
@click="handleManualRefresh"
|
||||||
/>
|
/>
|
||||||
<TimeRangePicker v-model="timeRange" />
|
<TimeRangePicker
|
||||||
|
v-model="timeRange"
|
||||||
|
:show-granularity="false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,10 +33,10 @@
|
|||||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-sm font-semibold">
|
<h2 class="text-sm font-semibold">
|
||||||
实时性能面板
|
实时运行状态
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
聚合系统状态、并发门、Tunnel 与 fallback 指标
|
聚合系统健康、并发保护、代理通道与降级切换
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
@@ -98,19 +101,30 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
<div class="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
||||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:col-span-2">
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:col-span-2">
|
||||||
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4 lg:col-span-2">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h3 class="text-sm font-semibold">
|
<h3 class="text-sm font-semibold">
|
||||||
本地并发门
|
并发保护
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
gateway_requests
|
全局 {{ distributedGateText }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||||
|
<div class="rounded-lg border border-border/60 bg-background/50 px-3 py-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="text-sm font-medium">
|
||||||
|
当前节点
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">
|
||||||
|
本机
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
In Flight
|
处理中
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.local.inFlight) }}
|
{{ formatMetricNumber(gatewayMetrics?.local.inFlight) }}
|
||||||
@@ -118,7 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Available
|
可接入
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.local.availablePermits) }}
|
{{ formatMetricNumber(gatewayMetrics?.local.availablePermits) }}
|
||||||
@@ -126,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
High Watermark
|
峰值并发
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.local.highWatermark) }}
|
{{ formatMetricNumber(gatewayMetrics?.local.highWatermark) }}
|
||||||
@@ -134,20 +148,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Rejected Total
|
被限流
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.local.rejectedTotal) }}
|
{{ formatMetricNumber(gatewayMetrics?.local.rejectedTotal) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
<div class="rounded-lg border border-border/60 bg-background/50 px-3 py-3">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h3 class="text-sm font-semibold">
|
<div class="text-sm font-medium">
|
||||||
分布式并发门
|
全局保护
|
||||||
</h3>
|
</div>
|
||||||
<Badge :variant="distributedGateVariant">
|
<Badge :variant="distributedGateVariant">
|
||||||
{{ distributedGateText }}
|
{{ distributedGateText }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -155,7 +169,7 @@
|
|||||||
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
In Flight
|
处理中
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.distributed.inFlight) }}
|
{{ formatMetricNumber(gatewayMetrics?.distributed.inFlight) }}
|
||||||
@@ -163,7 +177,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Available
|
可接入
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.distributed.availablePermits) }}
|
{{ formatMetricNumber(gatewayMetrics?.distributed.availablePermits) }}
|
||||||
@@ -171,7 +185,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
High Watermark
|
峰值并发
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.distributed.highWatermark) }}
|
{{ formatMetricNumber(gatewayMetrics?.distributed.highWatermark) }}
|
||||||
@@ -179,25 +193,27 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Rejected Total
|
被限流
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(gatewayMetrics?.distributed.rejectedTotal) }}
|
{{ formatMetricNumber(gatewayMetrics?.distributed.rejectedTotal) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="gatewayMetrics?.distributed.unavailable"
|
v-if="gatewayMetrics?.distributed.unavailable"
|
||||||
class="mt-3 text-xs text-yellow-700 dark:text-yellow-300"
|
class="mt-3 text-xs text-yellow-700 dark:text-yellow-300"
|
||||||
>
|
>
|
||||||
Redis 分布式并发快照当前不可用,需要检查 gate 后端连接。
|
全局并发保护暂不可用,请检查 Redis 连接。
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h3 class="text-sm font-semibold">
|
<h3 class="text-sm font-semibold">
|
||||||
Tunnel / 代理
|
代理通道
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
实时连接
|
实时连接
|
||||||
@@ -206,7 +222,7 @@
|
|||||||
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Nodes
|
节点数
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(currentTunnelNodes) }}
|
{{ formatMetricNumber(currentTunnelNodes) }}
|
||||||
@@ -214,15 +230,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Proxy Connections
|
可用连接
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(currentProxyConnections) }}
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.availableProxyConnections) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Active Streams
|
活跃流
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(currentActiveStreams) }}
|
{{ formatMetricNumber(currentActiveStreams) }}
|
||||||
@@ -230,10 +246,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Service Up
|
避让连接
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ gatewayMetrics?.serviceUp === 1 ? '在线' : '未知' }}
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.softAvoidProxyConnections) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,43 +258,43 @@
|
|||||||
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h3 class="text-sm font-semibold">
|
<h3 class="text-sm font-semibold">
|
||||||
今日请求概况
|
代理通道压力
|
||||||
</h3>
|
</h3>
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{{ systemStatus?.internal_gateway.status || 'gateway' }}
|
排队 {{ tunnelQueueUtilizationText }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Requests
|
排队中
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatMetricNumber(systemStatus?.today_stats.requests) }}
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.outboundQueueDepthTotal) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Tokens
|
峰值排队
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ formatTokens(systemStatus?.today_stats.tokens) }}
|
{{ formatMetricNumber(gatewayMetrics?.tunnel.outboundQueueDepthMax) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Cost
|
队列满拒绝
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ systemStatus?.today_stats.cost_usd || '-' }}
|
{{ formatMetricNumber(tunnelQueueRejectedTotal) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-muted-foreground">
|
<div class="text-xs text-muted-foreground">
|
||||||
Active Providers / Keys
|
无可用通道
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 text-lg font-semibold">
|
<div class="mt-1 text-lg font-semibold">
|
||||||
{{ providerAndKeySummary }}
|
{{ formatMetricNumber(tunnelSelectionPressureTotal) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,7 +304,7 @@
|
|||||||
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
<section class="rounded-xl border border-border/70 bg-card/60 p-4">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h3 class="text-sm font-semibold">
|
<h3 class="text-sm font-semibold">
|
||||||
Fallback 统计
|
降级切换统计
|
||||||
</h3>
|
</h3>
|
||||||
<span class="text-xs text-muted-foreground">
|
<span class="text-xs text-muted-foreground">
|
||||||
总计 {{ formatMetricNumber(gatewayMetrics?.fallbackTotal) }}
|
总计 {{ formatMetricNumber(gatewayMetrics?.fallbackTotal) }}
|
||||||
@@ -299,7 +315,7 @@
|
|||||||
v-if="!fallbackRows.length"
|
v-if="!fallbackRows.length"
|
||||||
class="mt-4 rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
class="mt-4 rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
当前没有记录到 fallback 计数。
|
当前没有记录到降级切换。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -372,7 +388,7 @@
|
|||||||
HTTP {{ item.context.status_code ?? '-' }}
|
HTTP {{ item.context.status_code ?? '-' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{{ item.context.provider_name || item.context.provider_id || '未知 Provider' }}
|
{{ item.context.provider_name || item.context.provider_id || '未知上游' }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
{{ item.context.api_format || item.context.model || '未知格式' }}
|
{{ item.context.api_format || item.context.model || '未知格式' }}
|
||||||
@@ -495,15 +511,90 @@
|
|||||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-sm font-semibold">
|
<h3 class="text-sm font-semibold">
|
||||||
Provider 性能
|
上游服务性能
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
{{ providerPerformanceSubtitle }}
|
{{ providerPerformanceSubtitle }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<Badge variant="outline">
|
<Badge variant="outline">
|
||||||
Top {{ providerPerformanceRows.length || 0 }}
|
Top {{ providerPerformanceRows.length || 0 }}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
<Button
|
||||||
|
v-if="hasProviderPerformanceFilters"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 gap-1 px-2 text-xs"
|
||||||
|
title="清除上游服务性能筛选"
|
||||||
|
@click="resetProviderPerformanceFilters"
|
||||||
|
>
|
||||||
|
<FilterX class="h-3.5 w-3.5" />
|
||||||
|
清除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-2 pt-1 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-7">
|
||||||
|
<Input
|
||||||
|
v-model="providerPerformanceProviderId"
|
||||||
|
size="sm"
|
||||||
|
placeholder="上游 ID"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="providerPerformanceModel"
|
||||||
|
size="sm"
|
||||||
|
placeholder="模型"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="providerPerformanceApiFormat"
|
||||||
|
size="sm"
|
||||||
|
placeholder="API 格式"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
v-model="providerPerformanceEndpointKind"
|
||||||
|
size="sm"
|
||||||
|
placeholder="端点类型"
|
||||||
|
/>
|
||||||
|
<Select v-model="providerPerformanceIsStream">
|
||||||
|
<SelectTrigger class="h-8 text-xs border-border/60">
|
||||||
|
<SelectValue placeholder="流式" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">
|
||||||
|
全部流式
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="true">
|
||||||
|
仅流式
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="false">
|
||||||
|
非流式
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select v-model="providerPerformanceHasFormatConversion">
|
||||||
|
<SelectTrigger class="h-8 text-xs border-border/60">
|
||||||
|
<SelectValue placeholder="格式转换" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">
|
||||||
|
全部转换
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="true">
|
||||||
|
仅转换
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="false">
|
||||||
|
不转换
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
v-model="providerPerformanceSlowThresholdMs"
|
||||||
|
size="sm"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="600000"
|
||||||
|
placeholder="慢请求阈值 ms"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -548,7 +639,7 @@
|
|||||||
<thead class="bg-muted/30 text-xs text-muted-foreground">
|
<thead class="bg-muted/30 text-xs text-muted-foreground">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-3 py-2 text-left font-medium">
|
<th class="px-3 py-2 text-left font-medium">
|
||||||
Provider
|
上游服务
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-2 text-right font-medium">
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
请求
|
请求
|
||||||
@@ -556,6 +647,9 @@
|
|||||||
<th class="px-3 py-2 text-right font-medium">
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
成功率
|
成功率
|
||||||
</th>
|
</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
|
错误率
|
||||||
|
</th>
|
||||||
<th class="px-3 py-2 text-right font-medium">
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
输出 TPS
|
输出 TPS
|
||||||
</th>
|
</th>
|
||||||
@@ -566,10 +660,16 @@
|
|||||||
平均响应
|
平均响应
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-2 text-right font-medium">
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
P90 响应 / 首字
|
P90/P99 响应
|
||||||
</th>
|
</th>
|
||||||
<th class="px-3 py-2 text-right font-medium">
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
样本
|
P90/P99 首字
|
||||||
|
</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
|
慢请求
|
||||||
|
</th>
|
||||||
|
<th class="px-3 py-2 text-right font-medium">
|
||||||
|
样本覆盖
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -593,6 +693,9 @@
|
|||||||
<td class="px-3 py-2 text-right">
|
<td class="px-3 py-2 text-right">
|
||||||
{{ formatProviderPerformanceMetric(provider.success_rate, '%') }}
|
{{ formatProviderPerformanceMetric(provider.success_rate, '%') }}
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-3 py-2 text-right">
|
||||||
|
{{ formatErrorRate(provider.success_rate) }}
|
||||||
|
</td>
|
||||||
<td class="px-3 py-2 text-right">
|
<td class="px-3 py-2 text-right">
|
||||||
{{ formatProviderPerformanceMetric(provider.avg_output_tps, ' tps') }}
|
{{ formatProviderPerformanceMetric(provider.avg_output_tps, ' tps') }}
|
||||||
</td>
|
</td>
|
||||||
@@ -605,11 +708,18 @@
|
|||||||
<td class="px-3 py-2 text-right">
|
<td class="px-3 py-2 text-right">
|
||||||
{{ formatProviderPerformanceMetric(provider.p90_response_time_ms, 'ms', 0) }}
|
{{ formatProviderPerformanceMetric(provider.p90_response_time_ms, 'ms', 0) }}
|
||||||
/
|
/
|
||||||
|
{{ formatProviderPerformanceMetric(provider.p99_response_time_ms, 'ms', 0) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 text-right">
|
||||||
{{ formatProviderPerformanceMetric(provider.p90_first_byte_time_ms, 'ms', 0) }}
|
{{ formatProviderPerformanceMetric(provider.p90_first_byte_time_ms, 'ms', 0) }}
|
||||||
|
/
|
||||||
|
{{ formatProviderPerformanceMetric(provider.p99_first_byte_time_ms, 'ms', 0) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 text-right">
|
||||||
|
{{ formatMetricNumber(provider.slow_request_count) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-right text-xs text-muted-foreground">
|
<td class="px-3 py-2 text-right text-xs text-muted-foreground">
|
||||||
{{ formatMetricNumber(provider.tps_sample_count) }} /
|
{{ providerSampleCoverageText(provider) }}
|
||||||
{{ formatMetricNumber(provider.first_byte_sample_count) }}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -620,7 +730,7 @@
|
|||||||
v-else
|
v-else
|
||||||
class="rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
class="rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
||||||
>
|
>
|
||||||
当前没有 Provider 性能数据。
|
当前没有上游服务性能数据。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -695,41 +805,6 @@
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card class="space-y-3 p-4">
|
|
||||||
<h3 class="text-sm font-semibold">
|
|
||||||
提供商健康度
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
v-if="providerLoading"
|
|
||||||
class="p-4"
|
|
||||||
>
|
|
||||||
<LoadingState />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="providerStatus.length"
|
|
||||||
class="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2 lg:grid-cols-3"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="provider in providerStatus"
|
|
||||||
:key="provider.name"
|
|
||||||
class="rounded-lg border p-3"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<span class="font-medium">{{ provider.name }}</span>
|
|
||||||
<span class="text-xs text-muted-foreground">{{ provider.requests }} 请求</span>
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-xs text-muted-foreground">
|
|
||||||
状态: {{ provider.status }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="rounded-lg border border-dashed border-border/70 px-3 py-4 text-sm text-muted-foreground"
|
|
||||||
>
|
|
||||||
当前没有提供商状态数据。
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -740,6 +815,7 @@ import {
|
|||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Cable,
|
Cable,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
FilterX,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Gauge,
|
Gauge,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
@@ -751,9 +827,9 @@ import {
|
|||||||
adminApi,
|
adminApi,
|
||||||
type ErrorDistributionResponse,
|
type ErrorDistributionResponse,
|
||||||
type PercentileItem,
|
type PercentileItem,
|
||||||
|
type ProviderPerformanceItem,
|
||||||
type ProviderPerformanceResponse,
|
type ProviderPerformanceResponse,
|
||||||
} from '@/api/admin'
|
} from '@/api/admin'
|
||||||
import { dashboardApi, type ProviderStatus } from '@/api/dashboard'
|
|
||||||
import {
|
import {
|
||||||
monitoringApi,
|
monitoringApi,
|
||||||
type AdminMonitoringCircuitHistoryItem,
|
type AdminMonitoringCircuitHistoryItem,
|
||||||
@@ -765,12 +841,19 @@ import LineChart from '@/components/charts/LineChart.vue'
|
|||||||
import { LoadingState, TimeRangePicker } from '@/components/common'
|
import { LoadingState, TimeRangePicker } from '@/components/common'
|
||||||
import { ErrorDistributionChart, PercentileChart } from '@/components/stats'
|
import { ErrorDistributionChart, PercentileChart } from '@/components/stats'
|
||||||
import Badge from '@/components/ui/badge.vue'
|
import Badge from '@/components/ui/badge.vue'
|
||||||
|
import Button from '@/components/ui/button.vue'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
|
import Input from '@/components/ui/input.vue'
|
||||||
import RefreshButton from '@/components/ui/refresh-button.vue'
|
import RefreshButton from '@/components/ui/refresh-button.vue'
|
||||||
|
import Select from '@/components/ui/select.vue'
|
||||||
|
import SelectContent from '@/components/ui/select-content.vue'
|
||||||
|
import SelectItem from '@/components/ui/select-item.vue'
|
||||||
|
import SelectTrigger from '@/components/ui/select-trigger.vue'
|
||||||
|
import SelectValue from '@/components/ui/select-value.vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
import { getDateRangeFromPeriod } from '@/features/usage/composables'
|
||||||
import type { DateRangeParams } from '@/features/usage/types'
|
import type { DateRangeParams } from '@/features/usage/types'
|
||||||
import { formatDate, formatNumber, formatTokens } from '@/utils/format'
|
import { formatDate, formatNumber } from '@/utils/format'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
import {
|
import {
|
||||||
buildProviderPerformanceChartData,
|
buildProviderPerformanceChartData,
|
||||||
@@ -778,6 +861,10 @@ import {
|
|||||||
} from './performanceAnalysisHelpers'
|
} from './performanceAnalysisHelpers'
|
||||||
|
|
||||||
const LIVE_REFRESH_INTERVAL_MS = 10_000
|
const LIVE_REFRESH_INTERVAL_MS = 10_000
|
||||||
|
const DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS = 10_000
|
||||||
|
|
||||||
|
type ProviderPerformanceBooleanFilter = 'all' | 'true' | 'false'
|
||||||
|
type ProviderPerformanceParams = NonNullable<Parameters<typeof adminApi.getProviderPerformance>[0]>
|
||||||
|
|
||||||
const timeRange = ref<DateRangeParams>(getDateRangeFromPeriod('last30days'))
|
const timeRange = ref<DateRangeParams>(getDateRangeFromPeriod('last30days'))
|
||||||
const { error: showError } = useToast()
|
const { error: showError } = useToast()
|
||||||
@@ -789,10 +876,15 @@ const errorDistribution = ref<ErrorDistributionResponse['distribution']>([])
|
|||||||
const errorTrend = ref<ErrorDistributionResponse['trend']>([])
|
const errorTrend = ref<ErrorDistributionResponse['trend']>([])
|
||||||
const errorLoading = ref(false)
|
const errorLoading = ref(false)
|
||||||
|
|
||||||
const providerStatus = ref<ProviderStatus[]>([])
|
|
||||||
const providerLoading = ref(false)
|
|
||||||
const providerPerformance = ref<ProviderPerformanceResponse | null>(null)
|
const providerPerformance = ref<ProviderPerformanceResponse | null>(null)
|
||||||
const providerPerformanceLoading = ref(false)
|
const providerPerformanceLoading = ref(false)
|
||||||
|
const providerPerformanceProviderId = ref('')
|
||||||
|
const providerPerformanceModel = ref('')
|
||||||
|
const providerPerformanceApiFormat = ref('')
|
||||||
|
const providerPerformanceEndpointKind = ref('')
|
||||||
|
const providerPerformanceIsStream = ref<ProviderPerformanceBooleanFilter>('all')
|
||||||
|
const providerPerformanceHasFormatConversion = ref<ProviderPerformanceBooleanFilter>('all')
|
||||||
|
const providerPerformanceSlowThresholdMs = ref(String(DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS))
|
||||||
|
|
||||||
const systemStatus = ref<AdminMonitoringSystemStatus | null>(null)
|
const systemStatus = ref<AdminMonitoringSystemStatus | null>(null)
|
||||||
const resilienceStatus = ref<AdminMonitoringResilienceStatus | null>(null)
|
const resilienceStatus = ref<AdminMonitoringResilienceStatus | null>(null)
|
||||||
@@ -806,12 +898,12 @@ const liveLastUpdatedAt = ref<string | null>(null)
|
|||||||
|
|
||||||
let percentilesRequestId = 0
|
let percentilesRequestId = 0
|
||||||
let errorsRequestId = 0
|
let errorsRequestId = 0
|
||||||
let providersRequestId = 0
|
|
||||||
let providerPerformanceRequestId = 0
|
let providerPerformanceRequestId = 0
|
||||||
let liveRequestId = 0
|
let liveRequestId = 0
|
||||||
let loadAllPromise: Promise<void> | null = null
|
let loadAllPromise: Promise<void> | null = null
|
||||||
let hasPendingLoadAll = false
|
let hasPendingLoadAll = false
|
||||||
let loadAllDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
let loadAllDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let providerPerformanceDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let liveRefreshTimer: ReturnType<typeof setInterval> | null = null
|
let liveRefreshTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
function buildTimeRangeParams() {
|
function buildTimeRangeParams() {
|
||||||
@@ -824,6 +916,117 @@ function buildTimeRangeParams() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProviderPerformanceFilter(value: string): string | undefined {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
return trimmed ? trimmed : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseProviderPerformanceBooleanFilter(
|
||||||
|
value: ProviderPerformanceBooleanFilter
|
||||||
|
): boolean | undefined {
|
||||||
|
if (value === 'true') return true
|
||||||
|
if (value === 'false') return false
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampProviderPerformanceSlowThreshold(value: string): number {
|
||||||
|
const parsed = Number(value)
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS
|
||||||
|
}
|
||||||
|
return Math.min(600_000, Math.max(1, Math.round(parsed)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveProviderPerformanceGranularity(): 'day' | 'hour' {
|
||||||
|
if (timeRange.value.preset === 'today' || timeRange.value.preset === 'yesterday') {
|
||||||
|
return 'hour'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!timeRange.value.preset && timeRange.value.start_date && timeRange.value.end_date) {
|
||||||
|
return timeRange.value.start_date === timeRange.value.end_date ? 'hour' : 'day'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'day'
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerPerformanceSlowThresholdValue = computed(() => (
|
||||||
|
clampProviderPerformanceSlowThreshold(providerPerformanceSlowThresholdMs.value)
|
||||||
|
))
|
||||||
|
|
||||||
|
const providerPerformanceSlowThresholdLabel = computed(() => {
|
||||||
|
const value = providerPerformanceSlowThresholdValue.value
|
||||||
|
if (value >= 1000) {
|
||||||
|
const seconds = value / 1000
|
||||||
|
return `${Number.isInteger(seconds) ? seconds.toFixed(0) : seconds.toFixed(1)}s`
|
||||||
|
}
|
||||||
|
return `${value}ms`
|
||||||
|
})
|
||||||
|
|
||||||
|
const providerPerformanceActiveFilterCount = computed(() => {
|
||||||
|
const textFilterCount = [
|
||||||
|
providerPerformanceProviderId.value,
|
||||||
|
providerPerformanceModel.value,
|
||||||
|
providerPerformanceApiFormat.value,
|
||||||
|
providerPerformanceEndpointKind.value,
|
||||||
|
].filter(value => normalizeProviderPerformanceFilter(value)).length
|
||||||
|
|
||||||
|
const booleanFilterCount = [
|
||||||
|
providerPerformanceIsStream.value,
|
||||||
|
providerPerformanceHasFormatConversion.value,
|
||||||
|
].filter(value => value !== 'all').length
|
||||||
|
|
||||||
|
const thresholdFilterCount = providerPerformanceSlowThresholdValue.value === DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS
|
||||||
|
? 0
|
||||||
|
: 1
|
||||||
|
|
||||||
|
return textFilterCount + booleanFilterCount + thresholdFilterCount
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasProviderPerformanceFilters = computed(() => providerPerformanceActiveFilterCount.value > 0)
|
||||||
|
|
||||||
|
function buildProviderPerformanceParams(): ProviderPerformanceParams {
|
||||||
|
const params: ProviderPerformanceParams = {
|
||||||
|
...buildTimeRangeParams(),
|
||||||
|
granularity: resolveProviderPerformanceGranularity(),
|
||||||
|
limit: 8,
|
||||||
|
slow_threshold_ms: providerPerformanceSlowThresholdValue.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerId = normalizeProviderPerformanceFilter(providerPerformanceProviderId.value)
|
||||||
|
if (providerId) params.provider_id = providerId
|
||||||
|
|
||||||
|
const model = normalizeProviderPerformanceFilter(providerPerformanceModel.value)
|
||||||
|
if (model) params.model = model
|
||||||
|
|
||||||
|
const apiFormat = normalizeProviderPerformanceFilter(providerPerformanceApiFormat.value)
|
||||||
|
if (apiFormat) params.api_format = apiFormat
|
||||||
|
|
||||||
|
const endpointKind = normalizeProviderPerformanceFilter(providerPerformanceEndpointKind.value)
|
||||||
|
if (endpointKind) params.endpoint_kind = endpointKind
|
||||||
|
|
||||||
|
const isStream = parseProviderPerformanceBooleanFilter(providerPerformanceIsStream.value)
|
||||||
|
if (isStream !== undefined) params.is_stream = isStream
|
||||||
|
|
||||||
|
const hasFormatConversion = parseProviderPerformanceBooleanFilter(
|
||||||
|
providerPerformanceHasFormatConversion.value
|
||||||
|
)
|
||||||
|
if (hasFormatConversion !== undefined) {
|
||||||
|
params.has_format_conversion = hasFormatConversion
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetProviderPerformanceFilters() {
|
||||||
|
providerPerformanceProviderId.value = ''
|
||||||
|
providerPerformanceModel.value = ''
|
||||||
|
providerPerformanceApiFormat.value = ''
|
||||||
|
providerPerformanceEndpointKind.value = ''
|
||||||
|
providerPerformanceIsStream.value = 'all'
|
||||||
|
providerPerformanceHasFormatConversion.value = 'all'
|
||||||
|
providerPerformanceSlowThresholdMs.value = String(DEFAULT_PROVIDER_PERFORMANCE_SLOW_THRESHOLD_MS)
|
||||||
|
}
|
||||||
|
|
||||||
function formatMetricNumber(value: number | null | undefined): string {
|
function formatMetricNumber(value: number | null | undefined): string {
|
||||||
if (value == null || Number.isNaN(value)) {
|
if (value == null || Number.isNaN(value)) {
|
||||||
return '-'
|
return '-'
|
||||||
@@ -836,6 +1039,24 @@ function formatMetricNumber(value: number | null | undefined): string {
|
|||||||
return formatNumber(value)
|
return formatNumber(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatErrorRate(successRate: number | null | undefined): string {
|
||||||
|
if (successRate == null || Number.isNaN(successRate)) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
return `${Math.max(0, 100 - successRate).toFixed(2)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
function providerSampleCoverageText(provider: ProviderPerformanceItem): string {
|
||||||
|
if (!provider.request_count) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const responseSamples = provider.response_time_sample_count ?? 0
|
||||||
|
const firstByteSamples = provider.first_byte_sample_count ?? 0
|
||||||
|
const responseCoverage = Math.round(responseSamples / provider.request_count * 100)
|
||||||
|
const firstByteCoverage = Math.round(firstByteSamples / provider.request_count * 100)
|
||||||
|
return `${responseCoverage}% / ${firstByteCoverage}%`
|
||||||
|
}
|
||||||
|
|
||||||
async function loadPercentiles() {
|
async function loadPercentiles() {
|
||||||
const requestId = ++percentilesRequestId
|
const requestId = ++percentilesRequestId
|
||||||
percentileLoading.value = true
|
percentileLoading.value = true
|
||||||
@@ -874,39 +1095,17 @@ async function loadErrors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProviders() {
|
|
||||||
const requestId = ++providersRequestId
|
|
||||||
providerLoading.value = true
|
|
||||||
try {
|
|
||||||
const data = await dashboardApi.getProviderStatus()
|
|
||||||
if (requestId !== providersRequestId) return
|
|
||||||
providerStatus.value = data
|
|
||||||
} catch (error) {
|
|
||||||
if (requestId !== providersRequestId) return
|
|
||||||
providerStatus.value = []
|
|
||||||
log.error('加载提供商状态失败', error)
|
|
||||||
} finally {
|
|
||||||
if (requestId === providersRequestId) {
|
|
||||||
providerLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadProviderPerformance() {
|
async function loadProviderPerformance() {
|
||||||
const requestId = ++providerPerformanceRequestId
|
const requestId = ++providerPerformanceRequestId
|
||||||
providerPerformanceLoading.value = true
|
providerPerformanceLoading.value = true
|
||||||
try {
|
try {
|
||||||
const data = await adminApi.getProviderPerformance({
|
const data = await adminApi.getProviderPerformance(buildProviderPerformanceParams())
|
||||||
...buildTimeRangeParams(),
|
|
||||||
granularity: 'day',
|
|
||||||
limit: 8,
|
|
||||||
})
|
|
||||||
if (requestId !== providerPerformanceRequestId) return
|
if (requestId !== providerPerformanceRequestId) return
|
||||||
providerPerformance.value = data
|
providerPerformance.value = data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (requestId !== providerPerformanceRequestId) return
|
if (requestId !== providerPerformanceRequestId) return
|
||||||
providerPerformance.value = null
|
providerPerformance.value = null
|
||||||
log.error('加载 Provider 性能统计失败', error)
|
log.error('加载上游服务性能统计失败', error)
|
||||||
} finally {
|
} finally {
|
||||||
if (requestId === providerPerformanceRequestId) {
|
if (requestId === providerPerformanceRequestId) {
|
||||||
providerPerformanceLoading.value = false
|
providerPerformanceLoading.value = false
|
||||||
@@ -1034,7 +1233,7 @@ const healthStatusText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const metricsAvailabilityText = computed(() => (
|
const metricsAvailabilityText = computed(() => (
|
||||||
gatewayMetrics.value ? 'Prometheus 在线' : 'Prometheus 暂不可达'
|
gatewayMetrics.value ? '网关指标在线' : '网关指标暂不可达'
|
||||||
))
|
))
|
||||||
|
|
||||||
const distributedGateVariant = computed<'warning' | 'outline'>(() => (
|
const distributedGateVariant = computed<'warning' | 'outline'>(() => (
|
||||||
@@ -1061,11 +1260,26 @@ const currentTunnelNodes = computed(() => (
|
|||||||
gatewayMetrics.value?.tunnel.nodes ?? systemStatus.value?.tunnel.nodes ?? null
|
gatewayMetrics.value?.tunnel.nodes ?? systemStatus.value?.tunnel.nodes ?? null
|
||||||
))
|
))
|
||||||
|
|
||||||
const providerAndKeySummary = computed(() => {
|
const tunnelQueueRejectedTotal = computed(() => {
|
||||||
if (!systemStatus.value) {
|
const full = gatewayMetrics.value?.tunnel.outboundQueueRejectedFullTotal ?? 0
|
||||||
|
const closed = gatewayMetrics.value?.tunnel.outboundQueueRejectedClosedTotal ?? 0
|
||||||
|
return full + closed
|
||||||
|
})
|
||||||
|
|
||||||
|
const tunnelSelectionPressureTotal = computed(() => {
|
||||||
|
const congested = gatewayMetrics.value?.tunnel.proxyConnectionCongestedTotal ?? 0
|
||||||
|
const retry = gatewayMetrics.value?.tunnel.selectionRetryTotal ?? 0
|
||||||
|
const unavailable = gatewayMetrics.value?.tunnel.selectionUnavailableTotal ?? 0
|
||||||
|
return congested + retry + unavailable
|
||||||
|
})
|
||||||
|
|
||||||
|
const tunnelQueueUtilizationText = computed(() => {
|
||||||
|
const depth = gatewayMetrics.value?.tunnel.outboundQueueDepthTotal
|
||||||
|
const capacity = gatewayMetrics.value?.tunnel.outboundQueueCapacityTotal
|
||||||
|
if (depth == null || capacity == null || capacity <= 0) {
|
||||||
return '-'
|
return '-'
|
||||||
}
|
}
|
||||||
return `${systemStatus.value.providers.active}/${systemStatus.value.providers.total} · ${systemStatus.value.api_keys.active}/${systemStatus.value.api_keys.total}`
|
return `${Math.round(depth / capacity * 100)}%`
|
||||||
})
|
})
|
||||||
|
|
||||||
const fallbackRows = computed(() => {
|
const fallbackRows = computed(() => {
|
||||||
@@ -1085,39 +1299,69 @@ const providerPerformanceRows = computed(() => providerPerformance.value?.provid
|
|||||||
|
|
||||||
const providerPerformanceSubtitle = computed(() => {
|
const providerPerformanceSubtitle = computed(() => {
|
||||||
const requests = providerPerformance.value?.summary.request_count ?? 0
|
const requests = providerPerformance.value?.summary.request_count ?? 0
|
||||||
return `完成窗口内 ${formatMetricNumber(requests)} 个 Provider 请求样本`
|
const filters = providerPerformanceActiveFilterCount.value
|
||||||
|
const filterText = filters > 0 ? ` · ${filters} 个筛选条件` : ''
|
||||||
|
return `完成窗口内 ${formatMetricNumber(requests)} 个上游请求样本${filterText}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const providerPerformanceSummaryCards = computed(() => {
|
const providerPerformanceSummaryCards = computed(() => {
|
||||||
const summary = providerPerformance.value?.summary
|
const summary = providerPerformance.value?.summary
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
title: '请求样本',
|
||||||
|
value: formatMetricNumber(summary?.request_count),
|
||||||
|
hint: `${formatMetricNumber(providerPerformanceRows.value.length)} 个上游服务`,
|
||||||
|
icon: Activity,
|
||||||
|
iconClass: 'text-blue-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '成功率',
|
||||||
|
value: formatProviderPerformanceMetric(summary?.success_rate, '%'),
|
||||||
|
hint: `错误率 ${formatErrorRate(summary?.success_rate)}`,
|
||||||
|
icon: CheckCircle2,
|
||||||
|
iconClass: 'text-emerald-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'P99 响应',
|
||||||
|
value: formatProviderPerformanceMetric(summary?.p99_response_time_ms, 'ms', 0),
|
||||||
|
hint: `P90 ${formatProviderPerformanceMetric(summary?.p90_response_time_ms, 'ms', 0)}`,
|
||||||
|
icon: Gauge,
|
||||||
|
iconClass: 'text-violet-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'P99 首字',
|
||||||
|
value: formatProviderPerformanceMetric(summary?.p99_first_byte_time_ms, 'ms', 0),
|
||||||
|
hint: `P90 ${formatProviderPerformanceMetric(summary?.p90_first_byte_time_ms, 'ms', 0)}`,
|
||||||
|
icon: Timer,
|
||||||
|
iconClass: 'text-sky-500',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '输出 TPS',
|
title: '输出 TPS',
|
||||||
value: formatProviderPerformanceMetric(summary?.avg_output_tps, ' tps'),
|
value: formatProviderPerformanceMetric(summary?.avg_output_tps, ' tps'),
|
||||||
hint: `请求 ${formatMetricNumber(summary?.request_count)}`,
|
hint: `TPS 样本 ${formatMetricNumber(summary?.tps_sample_count)}`,
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
iconClass: 'text-amber-500',
|
iconClass: 'text-amber-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '平均首字',
|
title: '平均首字',
|
||||||
value: formatProviderPerformanceMetric(summary?.avg_first_byte_time_ms, 'ms'),
|
value: formatProviderPerformanceMetric(summary?.avg_first_byte_time_ms, 'ms'),
|
||||||
hint: '成功请求首字样本',
|
hint: `首字样本 ${formatMetricNumber(summary?.first_byte_sample_count)}`,
|
||||||
icon: Timer,
|
icon: Timer,
|
||||||
iconClass: 'text-sky-500',
|
iconClass: 'text-sky-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '平均响应',
|
title: '平均响应',
|
||||||
value: formatProviderPerformanceMetric(summary?.avg_response_time_ms, 'ms'),
|
value: formatProviderPerformanceMetric(summary?.avg_response_time_ms, 'ms'),
|
||||||
hint: '成功请求响应耗时',
|
hint: `响应样本 ${formatMetricNumber(summary?.response_time_sample_count)}`,
|
||||||
icon: Gauge,
|
icon: Gauge,
|
||||||
iconClass: 'text-violet-500',
|
iconClass: 'text-violet-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '成功率',
|
title: '慢请求',
|
||||||
value: formatProviderPerformanceMetric(summary?.success_rate, '%'),
|
value: formatMetricNumber(summary?.slow_request_count),
|
||||||
hint: `${formatMetricNumber(providerPerformanceRows.value.length)} 个 Provider`,
|
hint: `响应耗时 >= ${providerPerformanceSlowThresholdLabel.value}`,
|
||||||
icon: CheckCircle2,
|
icon: AlertTriangle,
|
||||||
iconClass: 'text-emerald-500',
|
iconClass: 'text-yellow-500',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -1162,7 +1406,7 @@ const liveSummaryCards = computed(() => [
|
|||||||
{
|
{
|
||||||
title: '系统健康',
|
title: '系统健康',
|
||||||
value: resilienceStatus.value ? `${resilienceStatus.value.health_score}/100` : '-',
|
value: resilienceStatus.value ? `${resilienceStatus.value.health_score}/100` : '-',
|
||||||
hint: `${healthStatusText.value} · 开路 ${formatMetricNumber(resilienceStatus.value?.error_statistics.open_circuit_breakers)}`,
|
hint: `${healthStatusText.value} · 熔断打开 ${formatMetricNumber(resilienceStatus.value?.error_statistics.open_circuit_breakers)}`,
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
iconClass: 'text-emerald-500',
|
iconClass: 'text-emerald-500',
|
||||||
},
|
},
|
||||||
@@ -1174,34 +1418,34 @@ const liveSummaryCards = computed(() => [
|
|||||||
iconClass: 'text-yellow-500',
|
iconClass: 'text-yellow-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '当前活跃流',
|
title: '代理活跃流',
|
||||||
value: formatMetricNumber(currentActiveStreams.value),
|
value: formatMetricNumber(currentActiveStreams.value),
|
||||||
hint: `代理连接 ${formatMetricNumber(currentProxyConnections.value)}`,
|
hint: `代理连接 ${formatMetricNumber(currentProxyConnections.value)}`,
|
||||||
icon: Cable,
|
icon: Cable,
|
||||||
iconClass: 'text-sky-500',
|
iconClass: 'text-sky-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '本地 In Flight',
|
title: '当前节点处理中',
|
||||||
value: formatMetricNumber(gatewayMetrics.value?.local.inFlight),
|
value: formatMetricNumber(gatewayMetrics.value?.local.inFlight),
|
||||||
hint: `剩余 permit ${formatMetricNumber(gatewayMetrics.value?.local.availablePermits)}`,
|
hint: `可接入 ${formatMetricNumber(gatewayMetrics.value?.local.availablePermits)}`,
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
iconClass: 'text-blue-500',
|
iconClass: 'text-blue-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '分布式 In Flight',
|
title: '全局处理中',
|
||||||
value: gatewayMetrics.value?.distributed.unavailable
|
value: gatewayMetrics.value?.distributed.unavailable
|
||||||
? '不可用'
|
? '不可用'
|
||||||
: formatMetricNumber(gatewayMetrics.value?.distributed.inFlight),
|
: formatMetricNumber(gatewayMetrics.value?.distributed.inFlight),
|
||||||
hint: gatewayMetrics.value?.distributed.unavailable
|
hint: gatewayMetrics.value?.distributed.unavailable
|
||||||
? '检查 Redis gate 状态'
|
? '检查 Redis 连接'
|
||||||
: `剩余 permit ${formatMetricNumber(gatewayMetrics.value?.distributed.availablePermits)}`,
|
: `可接入 ${formatMetricNumber(gatewayMetrics.value?.distributed.availablePermits)}`,
|
||||||
icon: Workflow,
|
icon: Workflow,
|
||||||
iconClass: 'text-violet-500',
|
iconClass: 'text-violet-500',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Fallback 累计',
|
title: '降级切换',
|
||||||
value: formatMetricNumber(gatewayMetrics.value?.fallbackTotal),
|
value: formatMetricNumber(gatewayMetrics.value?.fallbackTotal),
|
||||||
hint: `今日请求 ${formatMetricNumber(systemStatus.value?.today_stats.requests)}`,
|
hint: '当前进程累计',
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
iconClass: 'text-rose-500',
|
iconClass: 'text-rose-500',
|
||||||
},
|
},
|
||||||
@@ -1212,7 +1456,6 @@ const isRefreshing = computed(() => (
|
|||||||
liveRefreshing.value ||
|
liveRefreshing.value ||
|
||||||
percentileLoading.value ||
|
percentileLoading.value ||
|
||||||
errorLoading.value ||
|
errorLoading.value ||
|
||||||
providerLoading.value ||
|
|
||||||
providerPerformanceLoading.value
|
providerPerformanceLoading.value
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -1225,7 +1468,6 @@ async function loadAll() {
|
|||||||
loadAllPromise = Promise.all([
|
loadAllPromise = Promise.all([
|
||||||
loadPercentiles(),
|
loadPercentiles(),
|
||||||
loadErrors(),
|
loadErrors(),
|
||||||
loadProviders(),
|
|
||||||
loadProviderPerformance(),
|
loadProviderPerformance(),
|
||||||
])
|
])
|
||||||
.then(() => undefined)
|
.then(() => undefined)
|
||||||
@@ -1255,7 +1497,30 @@ function scheduleLoadAll() {
|
|||||||
}, 120)
|
}, 120)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleProviderPerformanceLoad() {
|
||||||
|
if (providerPerformanceDebounceTimer) {
|
||||||
|
clearTimeout(providerPerformanceDebounceTimer)
|
||||||
|
}
|
||||||
|
|
||||||
|
providerPerformanceDebounceTimer = setTimeout(() => {
|
||||||
|
providerPerformanceDebounceTimer = null
|
||||||
|
void loadProviderPerformance()
|
||||||
|
}, 180)
|
||||||
|
}
|
||||||
|
|
||||||
watch(timeRange, scheduleLoadAll, { deep: true })
|
watch(timeRange, scheduleLoadAll, { deep: true })
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
providerPerformanceProviderId,
|
||||||
|
providerPerformanceModel,
|
||||||
|
providerPerformanceApiFormat,
|
||||||
|
providerPerformanceEndpointKind,
|
||||||
|
providerPerformanceIsStream,
|
||||||
|
providerPerformanceHasFormatConversion,
|
||||||
|
providerPerformanceSlowThresholdMs,
|
||||||
|
],
|
||||||
|
scheduleProviderPerformanceLoad
|
||||||
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadLiveData()
|
void loadLiveData()
|
||||||
@@ -1271,6 +1536,11 @@ onUnmounted(() => {
|
|||||||
loadAllDebounceTimer = null
|
loadAllDebounceTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerPerformanceDebounceTimer) {
|
||||||
|
clearTimeout(providerPerformanceDebounceTimer)
|
||||||
|
providerPerformanceDebounceTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
if (liveRefreshTimer) {
|
if (liveRefreshTimer) {
|
||||||
clearInterval(liveRefreshTimer)
|
clearInterval(liveRefreshTimer)
|
||||||
liveRefreshTimer = null
|
liveRefreshTimer = null
|
||||||
@@ -1280,7 +1550,6 @@ onUnmounted(() => {
|
|||||||
loadAllPromise = null
|
loadAllPromise = null
|
||||||
percentilesRequestId += 1
|
percentilesRequestId += 1
|
||||||
errorsRequestId += 1
|
errorsRequestId += 1
|
||||||
providersRequestId += 1
|
|
||||||
providerPerformanceRequestId += 1
|
providerPerformanceRequestId += 1
|
||||||
liveRequestId += 1
|
liveRequestId += 1
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user