diff --git a/backend/crates/gateway-api/src/admin/observability/presenter.rs b/backend/crates/gateway-api/src/admin/observability/presenter.rs index 2d27d016b..fcb949803 100644 --- a/backend/crates/gateway-api/src/admin/observability/presenter.rs +++ b/backend/crates/gateway-api/src/admin/observability/presenter.rs @@ -686,20 +686,10 @@ pub(crate) fn trend_point_view( tokens_value: point.metrics.total_tokens, errors: format_compact_number(point.service_failure_count), errors_value: point.service_failure_count, - latency: display_duration(point.average_latency_ms), latency_value: point.average_latency_ms, - max_latency: display_duration(point.metrics.max_latency_ms), - max_latency_value: point.metrics.max_latency_ms, - min_latency: display_duration(point.metrics.min_latency_ms), - min_latency_value: point.metrics.min_latency_ms, success_rate: success_rate_value .map_or_else(|| "—".to_owned(), |value| format!("{value:.1}%")), success_rate_value, - first_token_p50_ms: point - .metrics - .first_token_latency_percentiles - .p50_ms - .map(|value| value.as_f64()), first_token_p95_ms: point .metrics .first_token_latency_percentiles @@ -711,20 +701,6 @@ pub(crate) fn trend_point_view( .p95_ms .map(|value| value.as_f64()), output_throughput_p50: point.metrics.output_throughput_p50, - admission_decision_p95_ms: point - .metrics - .admission_decision_percentiles - .p95_ms - .map(|value| value.as_f64()), - account_selection_wait_p95_ms: point - .metrics - .account_selection_wait_percentiles - .p95_ms - .map(|value| value.as_f64()), - capacity_utilization: point - .metrics - .capacity_utilization_p95_basis_points - .map(basis_points_rate), } } diff --git a/backend/crates/gateway-api/src/admin/observability/wire.rs b/backend/crates/gateway-api/src/admin/observability/wire.rs index e518755fc..4e4579374 100644 --- a/backend/crates/gateway-api/src/admin/observability/wire.rs +++ b/backend/crates/gateway-api/src/admin/observability/wire.rs @@ -306,21 +306,12 @@ pub struct TrendPointView { pub tokens_value: u64, pub errors: String, pub errors_value: u64, - pub latency: String, pub latency_value: Option, - pub max_latency: String, - pub max_latency_value: Option, - pub min_latency: String, - pub min_latency_value: Option, pub success_rate: String, pub success_rate_value: Option, - pub first_token_p50_ms: Option, pub first_token_p95_ms: Option, pub latency_p95_ms: Option, pub output_throughput_p50: Option, - pub admission_decision_p95_ms: Option, - pub account_selection_wait_p95_ms: Option, - pub capacity_utilization: Option, } /// 趋势指标的稳定身份,展示名称由管理端本地化 diff --git a/backend/crates/gateway-store/src/postgres/observability/analytics/aggregate.rs b/backend/crates/gateway-store/src/postgres/observability/analytics/aggregate.rs index c076b39f7..4da5cbe13 100644 --- a/backend/crates/gateway-store/src/postgres/observability/analytics/aggregate.rs +++ b/backend/crates/gateway-store/src/postgres/observability/analytics/aggregate.rs @@ -89,8 +89,10 @@ pub(super) fn push_fact_aggregation_selected( } query.push(") sample(metric,value) where value is not null), sample_stats as (select output_bucket,name,metric,count(*) as count,sum(value) as sum,min(value) as minimum,max(value) as maximum,count(*) filter(where value=0) as zero"); query.push(" from samples group by output_bucket,name,metric)"); - // 对数桶定义与 Histogram::add 相同,零值单独保留,不对零取对数 - query.push(", bin_counts as (select output_bucket,name,metric,floor(log(2::numeric,value::numeric)*16)::integer as bin,count(*) as count from samples where value>0 group by output_bucket,name,metric,bin), merged_bins as (select output_bucket,name,metric,jsonb_object_agg(bin,count) as bins from bin_counts group by output_bucket,name,metric)"); + // 先合并同值样本,避免逐条重复计算 numeric 对数,保持原有桶边界和样本权重 + // 零值仍由 sample_stats 单独保留,不参与对数计算 + query.push(", sample_values as materialized (select output_bucket,name,metric,value,count(*) as count from samples where value>0 group by output_bucket,name,metric,value)"); + query.push(", bin_counts as (select output_bucket,name,metric,floor(log(2::numeric,value::numeric)*16)::integer as bin,sum(count)::bigint as count from sample_values group by output_bucket,name,metric,bin), merged_bins as (select output_bucket,name,metric,jsonb_object_agg(bin,count) as bins from bin_counts group by output_bucket,name,metric)"); query.push(", merged_distributions as (select s.output_bucket,s.name,jsonb_object_agg(s.metric,jsonb_build_object('count',s.count,'sum',s.sum,'minimum',s.minimum,'maximum',s.maximum,'zero',s.zero,'bins',"); query.push("coalesce(b.bins,'{}'::jsonb)"); query.push(")) as distributions"); diff --git a/backend/crates/gateway-store/tests/postgres/analytics.rs b/backend/crates/gateway-store/tests/postgres/analytics.rs index 1c4fb8124..32ccc03cd 100644 --- a/backend/crates/gateway-store/tests/postgres/analytics.rs +++ b/backend/crates/gateway-store/tests/postgres/analytics.rs @@ -1692,16 +1692,18 @@ async fn daily_rank_counts_follow_rebuilds_rollbacks_deletions_and_dirty_days() } #[tokio::test] -async fn sql_edge_histograms_match_persisted_power_of_two_boundaries() { +async fn sql_edge_histograms_preserve_repeated_samples_and_power_of_two_boundaries() { let Some(database) = TestDatabase::create("analytics_sql_histogram_boundaries").await else { return; }; let start = DateTime::parse_from_rfc3339("2026-09-01T10:00:00Z") .unwrap() .with_timezone(&Utc); - for (index, value) in [0_i64, 1, 2, 4, 8, 16, 31, 32, 33, 64, 128, 1024, 1_048_576] - .into_iter() - .enumerate() + for (index, value) in [ + 0_i64, 0, 1, 1, 1, 2, 2, 4, 8, 16, 31, 32, 32, 33, 64, 128, 1024, 1_048_576, + ] + .into_iter() + .enumerate() { let id = format!("sql_hist_{index}"); seed( diff --git a/docs/api.md b/docs/api.md index 55797f414..1fa94f310 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2022,6 +2022,10 @@ errorCode, errorMessage, startedAt, completedAt, expiresAt, createdAt, updatedAt 图表颜色、系列身份和选中状态使用 `metric`,不根据译文或数组位置推断指标 +`points[]` 提供趋势图与首页卡片使用的请求数、Tokens、缓存比例、错误数、成功率、 +`latencyValue`、首 Token P95、总延迟 P95 和输出吞吐 P50,以及时间标签与展示值 +内部调度分位数和容量分布通过用量洞察查询 + ### Dashboard 客户端身份 `wireProfiles[].attributes[]` 返回稳定的 `key` 和 `value`,字段名称由前端本地化 diff --git a/frontend/src/api/modules/dashboard.ts b/frontend/src/api/modules/dashboard.ts index a395e90d8..7bfd49658 100644 --- a/frontend/src/api/modules/dashboard.ts +++ b/frontend/src/api/modules/dashboard.ts @@ -26,21 +26,12 @@ export interface DashboardTrendPoint { tokensValue: number errors: string errorsValue: number - latency: string latencyValue: number | null - maxLatency: string - maxLatencyValue: number | null - minLatency: string - minLatencyValue: number | null successRate: string successRateValue: number | null - firstTokenP50Ms: number | null firstTokenP95Ms: number | null latencyP95Ms: number | null outputThroughputP50: number | null - admissionDecisionP95Ms: number | null - accountSelectionWaitP95Ms: number | null - capacityUtilization: number | null } export type DashboardTrendMetric diff --git a/frontend/src/views/theme/fixtures/dashboard.ts b/frontend/src/views/theme/fixtures/dashboard.ts index 472530e81..82d249039 100644 --- a/frontend/src/views/theme/fixtures/dashboard.ts +++ b/frontend/src/views/theme/fixtures/dashboard.ts @@ -81,21 +81,12 @@ function previewTrendPoint(requestsValue: number, index: number): DashboardTrend tokensValue: inputTokensValue + outputTokensValue, errors: errorsValue.toLocaleString('zh-CN'), errorsValue, - latency: `${410 + index * 7} ms`, latencyValue: 410 + index * 7, - maxLatency: `${760 + index * 12} ms`, - maxLatencyValue: 760 + index * 12, - minLatency: `${180 + index * 3} ms`, - minLatencyValue: 180 + index * 3, successRate: `${successRateValue.toFixed(1)}%`, successRateValue, - firstTokenP50Ms: 210 + index * 3, firstTokenP95Ms: 420 + index * 5, latencyP95Ms: 760 + index * 12, outputThroughputP50: 52 + (index % 5), - admissionDecisionP95Ms: 2 + (index % 3), - accountSelectionWaitP95Ms: 8 + (index % 4), - capacityUtilization: 0.42 + (index % 6) * 0.04, } }