mirror of
https://github.com/moeru-ai/airi.git
synced 2026-07-09 15:58:27 +00:00
chore(server): update Grafana dashboard layout
This commit is contained in:
parent
3779eb6103
commit
70b7f74339
3 changed files with 714 additions and 1561 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -58,14 +58,12 @@ describe('grafana dashboard builder', () => {
|
|||
* @example
|
||||
* expect(panelTitle('panel-99')).toBe('TTS Success %')
|
||||
*/
|
||||
it('keeps the product analytics row focused on server-side TTS health', () => {
|
||||
it('keeps the product analytics row focused on Prometheus-safe engagement signals', () => {
|
||||
expect(panelTitle('panel-95')).toBe('Product Events (range)')
|
||||
expect(panelTitle('panel-96')).toBe('Product Failure %')
|
||||
expect(panelTitle('panel-99')).toBe('TTS Success %')
|
||||
expect(panelTitle('panel-100')).toBe('TTS Failed / Blocked (range)')
|
||||
expect(panelTitle('panel-101')).toBe('TTS Event Rate by Source')
|
||||
expect(panelTitle('panel-102')).toBe('TTS Blocked by Reason')
|
||||
expect(panelTitle('panel-103')).toBe('TTS Blocked by Flux Bucket')
|
||||
expect(panelTitle('panel-97')).toBe('Top Product Actions (range)')
|
||||
expect(panelTitle('panel-98')).toBe('Product Event Rate')
|
||||
expect(panelTitle('panel-99')).toBe('日活')
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -80,10 +78,6 @@ describe('grafana dashboard builder', () => {
|
|||
dashboard.elements['panel-97'],
|
||||
dashboard.elements['panel-98'],
|
||||
dashboard.elements['panel-99'],
|
||||
dashboard.elements['panel-100'],
|
||||
dashboard.elements['panel-101'],
|
||||
dashboard.elements['panel-102'],
|
||||
dashboard.elements['panel-103'],
|
||||
]).join('\n')
|
||||
|
||||
expect(productPanelExpressions).not.toContain('voice_id')
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|||
|
||||
const PROM = { name: 'grafanacloud-projairi-prom' }
|
||||
const LOKI = { name: 'grafanacloud-projairi-logs' }
|
||||
const SCHEMA_VERSION = '13.0.0-23630096546'
|
||||
const SCHEMA_VERSION = '13.2.0-28666480772'
|
||||
|
||||
// Service / env filter applied to every Prom query. Pulled into a helper so
|
||||
// the variable name only appears once.
|
||||
|
|
@ -62,9 +62,10 @@ function query(expr: string, legend: string, refId = 'A', datasource: DataSource
|
|||
group: datasource === LOKI ? 'loki' : 'prometheus',
|
||||
kind: 'DataQuery',
|
||||
spec: {
|
||||
editorMode: 'code',
|
||||
expr,
|
||||
legendFormat: legend,
|
||||
...(opts.instant && { instant: true, range: false }),
|
||||
...(opts.instant ? { instant: true, range: false } : { range: true }),
|
||||
},
|
||||
version: 'v0',
|
||||
},
|
||||
|
|
@ -128,6 +129,8 @@ interface TimeseriesPanelOpts {
|
|||
stack?: boolean
|
||||
fillOpacity?: number
|
||||
legendCalcs?: LegendCalc[]
|
||||
legendPlacement?: 'bottom' | 'right'
|
||||
legendDisplayMode?: 'list' | 'table'
|
||||
}
|
||||
|
||||
// `noValue` shows a friendly placeholder instead of "No data" red text when
|
||||
|
|
@ -269,7 +272,14 @@ function barGaugePanel(id: number, title: string, description: string, queries:
|
|||
}
|
||||
|
||||
function timeseriesPanel(id: number, title: string, description: string, queries: PanelQuery[], opts: TimeseriesPanelOpts = {}) {
|
||||
const { unit = 'short', stack = false, fillOpacity = 20, legendCalcs = ['lastNotNull', 'max'] } = opts
|
||||
const {
|
||||
unit = 'short',
|
||||
stack = false,
|
||||
fillOpacity = 20,
|
||||
legendCalcs = ['lastNotNull', 'max'],
|
||||
legendPlacement = 'right',
|
||||
legendDisplayMode = 'table',
|
||||
} = opts
|
||||
return {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
|
|
@ -318,7 +328,14 @@ function timeseriesPanel(id: number, title: string, description: string, queries
|
|||
// Show last + max in the legend table so viewers don't have to
|
||||
// click each line to see numbers — same trick as Keycloak's
|
||||
// "Login Errors" panel.
|
||||
legend: { calcs: legendCalcs, displayMode: 'table', placement: 'right', showLegend: true },
|
||||
legend: {
|
||||
calcs: legendCalcs,
|
||||
displayMode: legendDisplayMode,
|
||||
enableFacetedFilter: false,
|
||||
overflow: 'ellipsis',
|
||||
placement: legendPlacement,
|
||||
showLegend: true,
|
||||
},
|
||||
tooltip: { hideZeros: false, mode: 'multi', sort: 'desc' },
|
||||
},
|
||||
},
|
||||
|
|
@ -328,6 +345,119 @@ function timeseriesPanel(id: number, title: string, description: string, queries
|
|||
}
|
||||
}
|
||||
|
||||
function pieChartPanel(id: number, title: string, description: string, queries: PanelQuery[], unit = 'short') {
|
||||
return {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
data: { kind: 'QueryGroup', spec: { queries, queryOptions: {}, transformations: [] } },
|
||||
description,
|
||||
id,
|
||||
links: [],
|
||||
title,
|
||||
vizConfig: {
|
||||
group: 'piechart',
|
||||
kind: 'VizConfig',
|
||||
spec: {
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
color: { fixedColor: '#73BF69', mode: 'palette-classic' },
|
||||
custom: { hideFrom: { legend: false, tooltip: false, viz: false } },
|
||||
unit,
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
options: {
|
||||
displayLabels: ['percent'],
|
||||
legend: { displayMode: 'table', overflow: 'ellipsis', placement: 'bottom', showLegend: true },
|
||||
pieType: 'pie',
|
||||
reduceOptions: { calcs: ['sum'], fields: '', values: false },
|
||||
sort: 'desc',
|
||||
tooltip: { hideZeros: false, mode: 'single', sort: 'none' },
|
||||
},
|
||||
},
|
||||
version: SCHEMA_VERSION,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Grafana UI panel retained as code so the generated dashboard matches
|
||||
// the latest hand-tuned cloud version without checking in cloud metadata.
|
||||
function dailyActiveUsersTrendPanel() {
|
||||
return {
|
||||
kind: 'Panel',
|
||||
spec: {
|
||||
data: {
|
||||
kind: 'QueryGroup',
|
||||
spec: {
|
||||
queries: [
|
||||
query(
|
||||
`max (user_active_rolling{${SERVICE_FILTER}, window="24h"})`,
|
||||
'__auto',
|
||||
),
|
||||
],
|
||||
queryOptions: {},
|
||||
transformations: [],
|
||||
},
|
||||
},
|
||||
description: '',
|
||||
id: 99,
|
||||
links: [],
|
||||
title: '日活',
|
||||
vizConfig: {
|
||||
group: 'timeseries',
|
||||
kind: 'VizConfig',
|
||||
spec: {
|
||||
fieldConfig: {
|
||||
defaults: {
|
||||
color: { mode: 'continuous-GrYlRd', seriesBy: 'last' },
|
||||
custom: {
|
||||
axisBorderShow: false,
|
||||
axisCenteredZero: false,
|
||||
axisColorMode: 'text',
|
||||
axisLabel: '',
|
||||
axisPlacement: 'auto',
|
||||
barAlignment: 0,
|
||||
barWidthFactor: 0.6,
|
||||
drawStyle: 'line',
|
||||
fillOpacity: 17,
|
||||
gradientMode: 'scheme',
|
||||
hideFrom: { legend: false, tooltip: false, viz: false },
|
||||
insertNulls: false,
|
||||
lineInterpolation: 'linear',
|
||||
lineStyle: { fill: 'solid' },
|
||||
lineWidth: 2,
|
||||
pointSize: 3,
|
||||
scaleDistribution: { type: 'linear' },
|
||||
showPoints: 'auto',
|
||||
showValues: false,
|
||||
spanNulls: false,
|
||||
stacking: { group: 'A', mode: 'none' },
|
||||
thresholdsStyle: { mode: 'off' },
|
||||
},
|
||||
thresholds: thresholds([{ color: 'green', value: 0 }, { color: 'red', value: 80 }]),
|
||||
},
|
||||
overrides: [],
|
||||
},
|
||||
options: {
|
||||
annotations: { clustering: -1, multiLane: false },
|
||||
legend: {
|
||||
calcs: [],
|
||||
displayMode: 'list',
|
||||
enableFacetedFilter: false,
|
||||
overflow: 'ellipsis',
|
||||
placement: 'bottom',
|
||||
showLegend: true,
|
||||
},
|
||||
tooltip: { hideZeros: false, mode: 'single', sort: 'none' },
|
||||
},
|
||||
},
|
||||
version: SCHEMA_VERSION,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
interface HeatmapPanelOpts {
|
||||
unit?: string
|
||||
}
|
||||
|
|
@ -577,69 +707,7 @@ elements['panel-98'] = timeseriesPanel(
|
|||
{ unit: 'eps', fillOpacity: 15 },
|
||||
)
|
||||
|
||||
elements['panel-99'] = gaugePanel(
|
||||
99,
|
||||
'TTS Success %',
|
||||
'Server-side TTS successes divided by TTS requests over the dashboard range. Includes REST and WS TTS product events. Drops here mean users are asking for speech but not receiving audio; inspect failed/blocked panels next.',
|
||||
[query(
|
||||
`100 * sum(increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}, feature="tts", action="speech_succeeded", status="succeeded"}[$__range])) / clamp_min(sum(increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}, feature="tts", action="speech_requested", status="started"}[$__range])), 1)`,
|
||||
'success %',
|
||||
)],
|
||||
{ steps: [{ color: 'red', value: 0 }, { color: 'yellow', value: 90 }, { color: 'green', value: 98 }], max: 100, decimals: 2, noValue: '0' },
|
||||
)
|
||||
|
||||
elements['panel-100'] = barGaugePanel(
|
||||
100,
|
||||
'TTS Failed / Blocked (range)',
|
||||
'TTS user-impacting failures over the dashboard range, split by action/status/source. `speech_failed` usually means upstream/runtime failure; `speech_blocked` usually means balance/preflight blocked. Keep voice/model drilldown in Postgres metadata, not Prometheus labels.',
|
||||
[query(
|
||||
`topk(12, sum by (action, status, source) (increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}, feature="tts", action=~"speech_failed|speech_blocked"}[$__range])))`,
|
||||
'{{action}} · {{status}} · {{source}}',
|
||||
'A',
|
||||
PROM,
|
||||
{ instant: true },
|
||||
)],
|
||||
{ unit: 'short', noValue: '0' },
|
||||
)
|
||||
|
||||
elements['panel-101'] = timeseriesPanel(
|
||||
101,
|
||||
'TTS Event Rate by Source',
|
||||
'TTS product event rate by source and action. Use this to distinguish chat auto-TTS, manual previews/settings tests, and API audio.speech traffic when speech health changes.',
|
||||
[query(
|
||||
`sum by (source, action, status) (rate(airi_product_events_total{${PRODUCT_EVENT_FILTER}, feature="tts"}[$__rate_interval]))`,
|
||||
'{{source}} · {{action}} · {{status}}',
|
||||
)],
|
||||
{ unit: 'eps', fillOpacity: 15 },
|
||||
)
|
||||
|
||||
elements['panel-102'] = barGaugePanel(
|
||||
102,
|
||||
'TTS Blocked by Reason',
|
||||
'Blocked TTS events over the dashboard range, grouped by bounded product reason and source. Today this mostly shows insufficient balance; new policy/provider/preflight buckets can be added without exposing user, voice, or model labels.',
|
||||
[query(
|
||||
`topk(12, sum by (reason, source) (increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}, feature="tts", action="speech_blocked", status="blocked"}[$__range])))`,
|
||||
'{{reason}} · {{source}}',
|
||||
'A',
|
||||
PROM,
|
||||
{ instant: true },
|
||||
)],
|
||||
{ unit: 'short', noValue: '0' },
|
||||
)
|
||||
|
||||
elements['panel-103'] = barGaugePanel(
|
||||
103,
|
||||
'TTS Blocked by Flux Bucket',
|
||||
'Blocked TTS events over the dashboard range, grouped by coarse Flux balance bucket. This helps separate truly empty accounts from low-balance accounts without exposing exact user balances.',
|
||||
[query(
|
||||
`topk(8, sum by (flux_balance_bucket, source) (increase(airi_product_events_total{${PRODUCT_EVENT_FILTER}, feature="tts", action="speech_blocked", status="blocked", flux_balance_bucket!=""}[$__range])))`,
|
||||
'{{flux_balance_bucket}} · {{source}}',
|
||||
'A',
|
||||
PROM,
|
||||
{ instant: true },
|
||||
)],
|
||||
{ unit: 'short', noValue: '0' },
|
||||
)
|
||||
elements['panel-99'] = dailyActiveUsersTrendPanel()
|
||||
|
||||
// --- Row 2: HTTP — traffic ranking, error trend, latency trend -------------
|
||||
elements['panel-16'] = barGaugePanel(
|
||||
|
|
@ -647,7 +715,7 @@ elements['panel-16'] = barGaugePanel(
|
|||
'Top Routes by Requests (range)',
|
||||
'Top Hono-matched routes by request count over the dashboard range. The main traffic list: which API surfaces are hottest. Wildcard patterns like `/api/v1/openai/*` are requests that did not reach a concrete handler (404 / auth-rejected); concrete paths are successful routes.',
|
||||
[query(
|
||||
`topk(10, sum by (http_route) (increase(http_server_request_duration_seconds_count{${SERVICE_FILTER}, http_request_method!="OPTIONS", http_route!=""}[$__range])))`,
|
||||
`topk(50, sum by (http_route) (increase(http_server_request_duration_seconds_count{${SERVICE_FILTER}, http_request_method!="OPTIONS", http_route!=""}[$__range])))`,
|
||||
'{{http_route}}',
|
||||
'A',
|
||||
PROM,
|
||||
|
|
@ -658,7 +726,7 @@ elements['panel-16'] = barGaugePanel(
|
|||
|
||||
elements['panel-40'] = heatmapPanel(
|
||||
40,
|
||||
'Error Rate %',
|
||||
'Status Distribution',
|
||||
'HTTP status-code mix over time, one row per status code, colour = request rate in each time bucket. The 200 row dominates in steady state; a 5xx / 4xx row suddenly lighting up flags an incident at a glance. Non-OPTIONS traffic only.',
|
||||
[query(
|
||||
`sum by (http_response_status_code) (rate(http_server_request_duration_seconds_count{${SERVICE_FILTER}, http_request_method!="OPTIONS"}[$__rate_interval]))`,
|
||||
|
|
@ -669,13 +737,15 @@ elements['panel-40'] = heatmapPanel(
|
|||
|
||||
elements['panel-20'] = timeseriesPanel(
|
||||
20,
|
||||
'HTTP P95 by Route',
|
||||
'P95 latency per Hono-matched route, excluding /api/v1/openai/* (LLM gateway latency lives in the LLM Gateway row). 404s excluded so missing-route noise does not skew the curve.',
|
||||
'Request Latency (by Route)',
|
||||
'',
|
||||
[query(
|
||||
`histogram_quantile(0.95, sum by (le, http_route) (rate(http_server_request_duration_seconds_bucket{${SERVICE_FILTER}, http_request_method!="OPTIONS", http_route!~"/api/v1/openai/.*", http_response_status_code!="404"}[$__rate_interval])))`,
|
||||
`sum by (http_route) (
|
||||
rate(http_server_request_duration_seconds_bucket{${SERVICE_FILTER}, http_request_method!="OPTIONS", http_route!~"/api/v1/openai/.*", http_response_status_code!="404"}[$__rate_interval])
|
||||
)`,
|
||||
'{{http_route}}',
|
||||
)],
|
||||
{ unit: 's' },
|
||||
{ unit: 's', legendPlacement: 'bottom' },
|
||||
)
|
||||
|
||||
elements['panel-94'] = timeseriesPanel(
|
||||
|
|
@ -690,7 +760,7 @@ elements['panel-94'] = timeseriesPanel(
|
|||
)
|
||||
|
||||
// --- Row 3: LLM Gateway — request mix + latency ----------------------------
|
||||
elements['panel-11'] = timeseriesPanel(
|
||||
elements['panel-11'] = pieChartPanel(
|
||||
11,
|
||||
'LLM Request Rate by Model',
|
||||
'Per-model request rate (chat + tts). Useful for capacity planning and spotting model-routing regressions.',
|
||||
|
|
@ -698,7 +768,7 @@ elements['panel-11'] = timeseriesPanel(
|
|||
`sum by (gen_ai_request_model) (rate(gen_ai_client_operation_count_total{${SERVICE_FILTER}, gen_ai_request_model!=""}[$__rate_interval]))`,
|
||||
'{{gen_ai_request_model}}',
|
||||
)],
|
||||
{ unit: 'reqps' },
|
||||
'reqps',
|
||||
)
|
||||
|
||||
elements['panel-21'] = timeseriesPanel(
|
||||
|
|
@ -706,7 +776,7 @@ elements['panel-21'] = timeseriesPanel(
|
|||
'LLM Latency P95',
|
||||
'Two P95 latency signals for the LLM gateway, aggregated across models. TTFB = time to first streamed token (streaming chat UX). End-to-end = full operation duration — the only latency signal for non-streaming chat and TTS, which have no first-token event.',
|
||||
[
|
||||
query(`histogram_quantile(0.95, sum by (le) (rate(gen_ai_client_first_token_duration_seconds_bucket{${SERVICE_FILTER}}[$__rate_interval])))`, 'TTFB p95', 'A'),
|
||||
query(`sum by () (rate(gen_ai_client_first_token_duration_seconds_bucket{${SERVICE_FILTER}}[$__rate_interval]))`, 'TTFB p95', 'A'),
|
||||
query(`histogram_quantile(0.95, sum by (le) (rate(gen_ai_client_operation_duration_seconds_bucket{${SERVICE_FILTER}}[$__rate_interval])))`, 'end-to-end p95', 'B'),
|
||||
],
|
||||
{ unit: 's' },
|
||||
|
|
@ -885,51 +955,6 @@ elements['panel-32'] = statPanel(
|
|||
{ unit: 'short', variant: 'count', noValue: '—', graphMode: 'none' },
|
||||
)
|
||||
|
||||
// --- Row 7: Infrastructure (collapsed) — process / DB health ---------------
|
||||
elements['panel-50'] = statPanel(
|
||||
50,
|
||||
'DB Query P95 (5m)',
|
||||
'PostgreSQL query duration P95 from PgInstrumentation. Fixed 5m window. Spikes correlate with index misses, connection exhaustion, or backend lock contention.',
|
||||
[query(
|
||||
`histogram_quantile(0.95, sum by (le) (rate(db_client_operation_duration_seconds_bucket{${SERVICE_FILTER}}[5m])))`,
|
||||
'p95',
|
||||
)],
|
||||
{ unit: 's', steps: [{ color: 'green', value: 0 }, { color: 'yellow', value: 0.05 }, { color: 'red', value: 0.5 }], decimals: 3 },
|
||||
)
|
||||
|
||||
elements['panel-51'] = timeseriesPanel(
|
||||
51,
|
||||
'DB Pool Connections by Instance',
|
||||
'Open PostgreSQL connections, per replica (`service_instance_id`). Each instance has its own pool sized by env `DB_POOL_MAX`. One instance with a permanently-high count = pool leak on that pod.',
|
||||
[query(
|
||||
`sum by (service_instance_id) (db_client_connection_count{${SERVICE_FILTER}})`,
|
||||
'{{service_instance_id}}',
|
||||
)],
|
||||
{ unit: 'short' },
|
||||
)
|
||||
|
||||
elements['panel-52'] = timeseriesPanel(
|
||||
52,
|
||||
'Heap Used % by Instance',
|
||||
'V8 heap used ÷ heap limit, per replica (`service_instance_id`). A single replica trending up while others stay flat = leak on that pod.',
|
||||
[query(
|
||||
`100 * sum by (service_instance_id) (v8js_memory_heap_used_bytes{${SERVICE_FILTER}}) / clamp_min(sum by (service_instance_id) (v8js_memory_heap_limit_bytes{${SERVICE_FILTER}}), 1)`,
|
||||
'{{service_instance_id}}',
|
||||
)],
|
||||
{ unit: 'percent' },
|
||||
)
|
||||
|
||||
elements['panel-53'] = timeseriesPanel(
|
||||
53,
|
||||
'Event Loop Delay P99 by Instance',
|
||||
'P99 event-loop delay per replica. One replica climbing while others stay flat = CPU-bound work pinning that pod. >50ms sustained is bad anywhere.',
|
||||
[query(
|
||||
`max by (service_instance_id) (nodejs_eventloop_delay_p99_seconds{${SERVICE_FILTER}})`,
|
||||
'{{service_instance_id}}',
|
||||
)],
|
||||
{ unit: 's' },
|
||||
)
|
||||
|
||||
// --- Row 8: Logs ------------------------------------------------------------
|
||||
elements['panel-91'] = logsPanel(
|
||||
91,
|
||||
|
|
@ -950,68 +975,53 @@ elements['panel-90'] = logsPanel(
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
const rows = [
|
||||
// Row 1: Service Health — two rows of glance stats + the status-code heatmap
|
||||
// standing tall on the right, with the live WS-connections trend full-width
|
||||
// underneath. counts (New Users / Active Sessions / WS Online) read blue with
|
||||
// a trend delta; req-rate + 5xx stay traffic-light.
|
||||
// Row 1: Service Health — dense single-screen operations layout from the
|
||||
// latest Grafana Cloud edit. Logs stay docked on the right while traffic,
|
||||
// latency, LLM, token, and provider panels fill the left.
|
||||
row('Service Health', [
|
||||
item('panel-1', 0, 0, 6, 4),
|
||||
item('panel-3', 6, 0, 6, 4),
|
||||
item('panel-4', 12, 0, 6, 4),
|
||||
item('panel-40', 18, 0, 6, 8),
|
||||
item('panel-15', 0, 4, 6, 4),
|
||||
item('panel-5', 6, 4, 6, 4),
|
||||
item('panel-93', 12, 4, 6, 4),
|
||||
item('panel-92', 0, 8, 24, 5),
|
||||
item('panel-3', 0, 0, 4, 5),
|
||||
item('panel-5', 4, 0, 4, 5),
|
||||
item('panel-40', 8, 0, 10, 6),
|
||||
item('panel-90', 18, 0, 6, 38),
|
||||
item('panel-93', 0, 5, 4, 5),
|
||||
item('panel-4', 4, 5, 4, 5),
|
||||
item('panel-20', 8, 6, 10, 15),
|
||||
item('panel-92', 0, 10, 4, 10),
|
||||
item('panel-73', 4, 10, 2, 10),
|
||||
item('panel-11', 6, 10, 2, 10),
|
||||
item('panel-16', 0, 20, 8, 18),
|
||||
item('panel-71', 8, 21, 5, 5),
|
||||
item('panel-21', 13, 21, 5, 5),
|
||||
item('panel-69', 8, 26, 5, 6),
|
||||
item('panel-67', 13, 26, 5, 6),
|
||||
item('panel-66', 8, 32, 5, 6),
|
||||
item('panel-68', 13, 32, 5, 6),
|
||||
]),
|
||||
// Row 2: User Engagement — rolling-window active users (DAU/WAU/MAU) from
|
||||
// user.last_seen_at. Kept its own row so it can grow (retention, cohorts)
|
||||
// without crowding the health glance above.
|
||||
// Row 2: User Engagement — rolling-window active users and Prom-safe product
|
||||
// analytics. The hand-tuned "日活" trend gives the row a visual engagement
|
||||
// anchor while compact stats keep user/session totals nearby.
|
||||
row('User Engagement', [
|
||||
item('panel-80', 0, 0, 8, 4),
|
||||
item('panel-81', 8, 0, 8, 4),
|
||||
item('panel-82', 16, 0, 8, 4),
|
||||
item('panel-80', 0, 0, 3, 5),
|
||||
item('panel-1', 3, 0, 3, 5),
|
||||
item('panel-99', 6, 0, 12, 11),
|
||||
item('panel-98', 18, 0, 6, 11),
|
||||
item('panel-82', 0, 5, 3, 3),
|
||||
item('panel-15', 3, 5, 3, 6),
|
||||
item('panel-81', 0, 8, 3, 3),
|
||||
item('panel-95', 0, 11, 6, 9),
|
||||
item('panel-96', 6, 11, 6, 9),
|
||||
item('panel-97', 12, 11, 12, 9),
|
||||
]),
|
||||
// Row 3: Product Analytics — Prom-safe event volume and server TTS health.
|
||||
// Distinct-user analytics stay in Postgres `product_events`; this row
|
||||
// intentionally never uses user_id/session/request labels.
|
||||
row('Product Analytics', [
|
||||
item('panel-95', 0, 0, 6, 5),
|
||||
item('panel-96', 6, 0, 6, 5),
|
||||
item('panel-99', 12, 0, 6, 5),
|
||||
item('panel-100', 18, 0, 6, 5),
|
||||
item('panel-97', 0, 5, 12, 8),
|
||||
item('panel-98', 12, 5, 12, 4),
|
||||
item('panel-101', 12, 9, 12, 4),
|
||||
item('panel-102', 0, 13, 12, 5),
|
||||
item('panel-103', 12, 13, 12, 5),
|
||||
]),
|
||||
// Row 3: HTTP — full-width error breakdown on top, then traffic ranking +
|
||||
// latency trend side by side.
|
||||
row('Product Analytics', []),
|
||||
// Row 3: HTTP — full-width error breakdown; traffic ranking and latency moved
|
||||
// into the dense Service Health screen above.
|
||||
row('HTTP', [
|
||||
item('panel-94', 0, 0, 24, 8),
|
||||
item('panel-16', 0, 8, 7, 11),
|
||||
item('panel-20', 7, 8, 17, 11),
|
||||
]),
|
||||
// Row 4: LLM gateway — request mix + latency side by side.
|
||||
row('LLM Gateway', [
|
||||
item('panel-11', 0, 0, 12, 8),
|
||||
item('panel-21', 12, 0, 12, 8),
|
||||
]),
|
||||
// Row 5: Provider Upstreams — per-provider rollup so the vendor consoles
|
||||
// don't have to be opened one by one. Four wide, one screen line.
|
||||
row('Provider Upstreams', [
|
||||
item('panel-66', 0, 0, 6, 7),
|
||||
item('panel-67', 6, 0, 6, 7),
|
||||
item('panel-68', 12, 0, 6, 7),
|
||||
item('panel-69', 18, 0, 6, 7),
|
||||
]),
|
||||
// Row 4: token totals + throughput + the two revenue/quality alert stats.
|
||||
row('LLM Tokens & Quality', [
|
||||
item('panel-73', 0, 0, 6, 7),
|
||||
item('panel-71', 6, 0, 10, 7),
|
||||
item('panel-43', 16, 0, 4, 7),
|
||||
item('panel-41', 20, 0, 4, 7),
|
||||
item('panel-43', 0, 0, 6, 7),
|
||||
item('panel-41', 6, 0, 6, 7),
|
||||
]),
|
||||
// Row 5: router health — three "wake someone up" stats/gauge + upstream errors.
|
||||
row('LLM Router Health', [
|
||||
|
|
@ -1026,17 +1036,10 @@ const rows = [
|
|||
item('panel-31', 8, 0, 8, 7),
|
||||
item('panel-32', 16, 0, 8, 7),
|
||||
]),
|
||||
// Row 7: infra (collapsed) — by-instance breakdowns catch single-replica issues.
|
||||
row('Infrastructure', [
|
||||
item('panel-50', 0, 0, 6, 6),
|
||||
item('panel-51', 6, 0, 6, 6),
|
||||
item('panel-52', 12, 0, 6, 6),
|
||||
item('panel-53', 18, 0, 6, 6),
|
||||
], { collapse: true }),
|
||||
// Row 8: full-width logs — errors on top (triage focus), firehose below.
|
||||
// Row 8: focused error logs; the live application firehose is docked in
|
||||
// Service Health for the latest cloud layout.
|
||||
row('Logs', [
|
||||
item('panel-91', 0, 0, 24, 10),
|
||||
item('panel-90', 0, 10, 24, 10),
|
||||
item('panel-91', 0, 0, 24, 8),
|
||||
]),
|
||||
]
|
||||
|
||||
|
|
@ -1105,18 +1108,15 @@ const variables = [
|
|||
* AIRI Server Overview dashboard.
|
||||
*
|
||||
* Reading order:
|
||||
* 1. Service Health — signup/sessions/WS counts, req-rate, 5xx, status-code
|
||||
* heatmap, live WS trend: "is anything broken right now?"
|
||||
* 2. User Engagement — rolling DAU/WAU/MAU from user.last_seen_at
|
||||
* 3. Product Analytics — Prom-safe product event volume + server TTS health
|
||||
* 4. HTTP — error breakdown by route, request ranking, latency by route
|
||||
* 5. LLM Gateway — per-model request rate + latency (TTFB + end-to-end)
|
||||
* 6. Provider Upstreams — per-provider rate/latency/failure + TTS chars
|
||||
* 7. LLM Tokens & Quality — token totals/throughput, revenue-leak alerts
|
||||
* 8. LLM Router Health — key/decrypt/fallback "wake someone up" signals
|
||||
* 9. Business — Stripe / Flux money flow
|
||||
* 10. Infrastructure (collapsed) — DB / runtime health for triage
|
||||
* 11. Logs — Loki for live debugging
|
||||
* 1. Service Health — dense operations screen with request, LLM, provider,
|
||||
* token, WebSocket, status, and live application-log signals.
|
||||
* 2. User Engagement — rolling DAU/WAU/MAU, total users, sessions, product
|
||||
* event health, and the hand-tuned daily-active-user trend.
|
||||
* 3. HTTP — full-width route error breakdown.
|
||||
* 4. LLM Tokens & Quality — revenue-leak and stream-interruption alerts.
|
||||
* 5. LLM Router Health — key/decrypt/fallback "wake someone up" signals.
|
||||
* 6. Business — Stripe / Flux money flow.
|
||||
* 7. Logs — Loki warning/error logs for live debugging.
|
||||
*
|
||||
* One metric, one panel: we deliberately do not duplicate a metric across
|
||||
* stat/trend/bar/pie forms. Counter conventions: rate() for "now" trends,
|
||||
|
|
@ -1154,10 +1154,10 @@ export const dashboard = {
|
|||
preload: false,
|
||||
tags: ['airi', 'observability', 'grafana-cloud'],
|
||||
timeSettings: {
|
||||
autoRefresh: '30s',
|
||||
autoRefresh: '',
|
||||
autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'],
|
||||
fiscalYearStartMonth: 0,
|
||||
from: 'now-1h',
|
||||
from: 'now-6h',
|
||||
hideTimepicker: false,
|
||||
timezone: 'browser',
|
||||
to: 'now',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue