@intentius/chant-lexicon-grafana 0.124.31 → 0.124.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/grafana.js +36 -0
- package/dist/actions/index.js +1 -0
- package/dist/alerting-build.js +262 -0
- package/dist/alerting.js +125 -0
- package/dist/annotations.js +47 -0
- package/dist/api/apply.js +320 -0
- package/dist/api/bind.js +44 -0
- package/dist/api/client.js +113 -0
- package/dist/api/converge.js +61 -0
- package/dist/api/dashboards.js +210 -0
- package/dist/api/datasources.js +24 -0
- package/dist/api/fake-grafana-writes.js +185 -0
- package/dist/api/fake-grafana.js +161 -0
- package/dist/api/folders.js +290 -0
- package/dist/api/library-panels.js +82 -0
- package/dist/build.js +767 -0
- package/dist/catalog.js +198 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +130 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +97 -0
- package/dist/codegen/package.js +28 -0
- package/dist/codegen/schema-types.js +159 -0
- package/dist/composites/agent-dashboard.js +460 -0
- package/dist/composites/catalog.js +306 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/red-dashboard.js +135 -0
- package/dist/composites/shared.js +74 -0
- package/dist/composites/slo-alert-rules.js +96 -0
- package/dist/composites/slo-dashboard.js +153 -0
- package/dist/config.js +110 -0
- package/dist/contact-point-check.js +61 -0
- package/dist/contact-point-secrets.js +32 -0
- package/dist/contact-point-settings.gen.js +421 -0
- package/dist/dashboard.js +60 -0
- package/dist/datasource-refs.js +170 -0
- package/dist/datasource-settings.js +57 -0
- package/dist/datasource.js +68 -0
- package/dist/deep-observe-hooks.js +266 -0
- package/dist/deep-observe.js +171 -0
- package/dist/describe-resources.js +249 -0
- package/dist/detect.js +119 -0
- package/dist/export-resources.js +73 -0
- package/dist/folder.js +86 -0
- package/dist/import/alerting.js +514 -0
- package/dist/import/edits.js +90 -0
- package/dist/import/embedded.js +60 -0
- package/dist/import/generator.js +656 -0
- package/dist/import/live-export.js +246 -0
- package/dist/import/mappings.js +317 -0
- package/dist/import/model.js +23 -0
- package/dist/import/normalize-alerting.js +106 -0
- package/dist/import/normalize.js +426 -0
- package/dist/import/operator.js +252 -0
- package/dist/import/parser.js +1480 -0
- package/dist/import/report.js +58 -0
- package/dist/import/testdata/fixtures.js +122 -0
- package/dist/import/v2.js +814 -0
- package/dist/index.js +59 -0
- package/dist/init-templates.js +305 -0
- package/dist/integrity.json +2 -2
- package/dist/k8s.js +548 -0
- package/dist/library-panel.js +83 -0
- package/dist/lint/audit-catalog.js +40 -0
- package/dist/lint/audit-lineage.js +9 -0
- package/dist/lint/post-synth/graf101.js +13 -0
- package/dist/lint/post-synth/graf102.js +13 -0
- package/dist/lint/post-synth/graf103.js +13 -0
- package/dist/lint/post-synth/graf104.js +13 -0
- package/dist/lint/post-synth/graf105.js +13 -0
- package/dist/lint/post-synth/graf106.js +13 -0
- package/dist/lint/post-synth/graf107.js +13 -0
- package/dist/lint/post-synth/graf108.js +13 -0
- package/dist/lint/post-synth/graf109.js +13 -0
- package/dist/lint/post-synth/graf110.js +13 -0
- package/dist/lint/post-synth/graf111.js +13 -0
- package/dist/lint/post-synth/graf112.js +13 -0
- package/dist/lint/post-synth/graf113.js +13 -0
- package/dist/lint/post-synth/graf114.js +13 -0
- package/dist/lint/post-synth/graf115.js +13 -0
- package/dist/lint/post-synth/graf116.js +13 -0
- package/dist/lint/post-synth/graf117.js +13 -0
- package/dist/lint/post-synth/graf118.js +37 -0
- package/dist/lint/post-synth/grafana-helpers.js +89 -0
- package/dist/lint/post-synth/index.js +38 -0
- package/dist/lint/rules/grafana-ast.js +70 -0
- package/dist/lint/rules/index.js +6 -0
- package/dist/lint/rules/literal-secret.js +109 -0
- package/dist/lint/rules/uid-syntax.js +53 -0
- package/dist/lsp/completions.js +11 -0
- package/dist/lsp/hover.js +29 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/grafana-apply.js +147 -0
- package/dist/op/activities/index.js +7 -0
- package/dist/op/builders.js +19 -0
- package/dist/ownership.js +102 -0
- package/dist/package-cli.js +14 -0
- package/dist/panel-options.js +12 -0
- package/dist/panels.js +296 -0
- package/dist/pin.js +113 -0
- package/dist/plugin.js +172 -0
- package/dist/promql-check.js +176 -0
- package/dist/query-models.js +13 -0
- package/dist/query-syntax.js +129 -0
- package/dist/query.js +227 -0
- package/dist/schema/annotationslist.gen.js +5 -0
- package/dist/schema/azuremonitor.gen.js +5 -0
- package/dist/schema/barchart.gen.js +5 -0
- package/dist/schema/bargauge.gen.js +5 -0
- package/dist/schema/bigquery.gen.js +5 -0
- package/dist/schema/candlestick.gen.js +5 -0
- package/dist/schema/canvas.gen.js +5 -0
- package/dist/schema/cloudwatch.gen.js +5 -0
- package/dist/schema/dashboard.gen.js +6 -0
- package/dist/schema/dashboardlist.gen.js +5 -0
- package/dist/schema/datagrid.gen.js +5 -0
- package/dist/schema/elasticsearch.gen.js +5 -0
- package/dist/schema/expr.gen.js +5 -0
- package/dist/schema/gauge.gen.js +5 -0
- package/dist/schema/geomap.gen.js +5 -0
- package/dist/schema/googlecloudmonitoring.gen.js +5 -0
- package/dist/schema/grafanapyroscope.gen.js +5 -0
- package/dist/schema/heatmap.gen.js +5 -0
- package/dist/schema/histogram.gen.js +5 -0
- package/dist/schema/index.js +40 -0
- package/dist/schema/logs.gen.js +5 -0
- package/dist/schema/loki.gen.js +5 -0
- package/dist/schema/news.gen.js +5 -0
- package/dist/schema/nodegraph.gen.js +5 -0
- package/dist/schema/piechart.gen.js +5 -0
- package/dist/schema/prometheus.gen.js +5 -0
- package/dist/schema/stat.gen.js +5 -0
- package/dist/schema/statetimeline.gen.js +5 -0
- package/dist/schema/statushistory.gen.js +5 -0
- package/dist/schema/table.gen.js +5 -0
- package/dist/schema/tempo.gen.js +5 -0
- package/dist/schema/text.gen.js +5 -0
- package/dist/schema/timeseries.gen.js +5 -0
- package/dist/schema/trend.gen.js +5 -0
- package/dist/schema/xychart.gen.js +5 -0
- package/dist/schema-validate.js +265 -0
- package/dist/serializer.js +40 -0
- package/dist/skill-defs.js +60 -0
- package/dist/spec/fetch-cli.js +26 -0
- package/dist/spec/fetch-notifiers-cli.js +53 -0
- package/dist/spec/fetch-units-cli.js +40 -0
- package/dist/spec/notifiers.js +126 -0
- package/dist/spec/overlay.js +92 -0
- package/dist/spec/schemas.gen.js +42 -0
- package/dist/spec/schemas.js +62 -0
- package/dist/spec/units.gen.js +280 -0
- package/dist/spec/units.js +72 -0
- package/dist/transformations.js +127 -0
- package/dist/util.js +34 -0
- package/dist/validate-alerting.js +444 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate-output.js +635 -0
- package/dist/validate.js +122 -0
- package/dist/validation.js +13 -0
- package/dist/variables.js +70 -0
- package/package.json +10 -10
- package/src/codegen/package.ts +2 -2
- package/src/spec/fetch-notifiers-cli.ts +2 -2
- package/src/spec/fetch-units-cli.ts +2 -2
- package/src/spec/overlay.ts +2 -2
- package/src/spec/schemas.ts +2 -2
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AgentDashboard`: latency and errors per model and per tool, and token
|
|
3
|
+
* usage per model, from the otel lexicon's GenAI preset.
|
|
4
|
+
*
|
|
5
|
+
* Two modes:
|
|
6
|
+
*
|
|
7
|
+
* - `genAi`: the preset's span metrics, read directly. Metric names come from
|
|
8
|
+
* `genAiMetrics()` (or the `metrics` of `genAiComponents()`), and label
|
|
9
|
+
* names from the preset's GenAI attribute keys, so a preset with another
|
|
10
|
+
* `namespace` moves every query here.
|
|
11
|
+
* - `rules`: the series the prometheus lexicon's `GenAiRules` records, named
|
|
12
|
+
* by `genAiRuleMetrics()`. These follow the conventions' client metrics
|
|
13
|
+
* when the collector emits them (#3041), and add a provider breakdown, cost
|
|
14
|
+
* from the declared price table and the alerts. Opt-in: a dashboard given
|
|
15
|
+
* only `genAi` is built exactly as before.
|
|
16
|
+
*/
|
|
17
|
+
import { Composite } from "@intentius/chant/composite";
|
|
18
|
+
import { GENAI_ATTRIBUTES, GENAI_TOKEN_TYPES } from "@intentius/chant-lexicon-otel/genai";
|
|
19
|
+
import { prometheusLabel, SPAN_STATUS_ERROR } from "@intentius/chant-lexicon-otel/metric-names";
|
|
20
|
+
import { genAiRuleMetrics } from "@intentius/chant-lexicon-prometheus/composites/genai";
|
|
21
|
+
import { GRAFANA_UNIT_IDS } from "../spec/units.gen.js";
|
|
22
|
+
import { Dashboard } from "../dashboard.js";
|
|
23
|
+
import { Row, StatPanel, TimeSeriesPanel } from "../panels.js";
|
|
24
|
+
import { PromQuery } from "../query.js";
|
|
25
|
+
import { QueryVariable } from "../variables.js";
|
|
26
|
+
import { slugUid } from "../util.js";
|
|
27
|
+
import { dashboardProps, durationUnit, errorRatio, legend, num, quantile, quantileName, requireDatasource, selector, sumRate, } from "./shared.js";
|
|
28
|
+
function metricsOf(genAi) {
|
|
29
|
+
const m = genAi.metrics ?? genAi;
|
|
30
|
+
if (!m || !m.calls || !m.duration || !m.inputTokens || !m.outputTokens) {
|
|
31
|
+
throw new Error("AgentDashboard: genAi must be genAiMetrics(...) or genAiComponents(...) from the otel lexicon");
|
|
32
|
+
}
|
|
33
|
+
return m;
|
|
34
|
+
}
|
|
35
|
+
function labelOf(metric, attribute) {
|
|
36
|
+
if (!metric.dimensions.includes(attribute)) {
|
|
37
|
+
throw new Error(`AgentDashboard: ${metric.name} has no ${attribute} dimension, so it can't be broken down by it`);
|
|
38
|
+
}
|
|
39
|
+
return prometheusLabel(attribute);
|
|
40
|
+
}
|
|
41
|
+
/** The PromQL the agent dashboard runs, from the preset's metrics. Exposed for tests and for panels of your own. */
|
|
42
|
+
export function agentQueries(m, q = 0.95) {
|
|
43
|
+
const model = labelOf(m.calls, GENAI_ATTRIBUTES.requestModel);
|
|
44
|
+
const tool = labelOf(m.calls, GENAI_ATTRIBUTES.toolName);
|
|
45
|
+
const errorType = labelOf(m.calls, GENAI_ATTRIBUTES.errorType);
|
|
46
|
+
const status = labelOf(m.calls, "status.code");
|
|
47
|
+
const service = labelOf(m.calls, "service.name");
|
|
48
|
+
labelOf(m.duration, GENAI_ATTRIBUTES.requestModel);
|
|
49
|
+
labelOf(m.duration, GENAI_ATTRIBUTES.toolName);
|
|
50
|
+
const tokenModel = labelOf(m.inputTokens, GENAI_ATTRIBUTES.requestModel);
|
|
51
|
+
labelOf(m.outputTokens, GENAI_ATTRIBUTES.requestModel);
|
|
52
|
+
const scope = [
|
|
53
|
+
[service, "=~", "$service"],
|
|
54
|
+
[model, "=~", "$model"],
|
|
55
|
+
];
|
|
56
|
+
const toolScope = [...scope, [tool, "!=", ""]];
|
|
57
|
+
const err = [status, "=", SPAN_STATUS_ERROR];
|
|
58
|
+
const calls = m.calls.prometheus;
|
|
59
|
+
const buckets = `${m.duration.prometheus}_bucket`;
|
|
60
|
+
const ratio = (matchers, by) => errorRatio(selector(calls, [...matchers, err]), selector(calls, matchers), by);
|
|
61
|
+
const tokens = [[tokenModel, "=~", "$model"]];
|
|
62
|
+
return {
|
|
63
|
+
labels: { model, tool, errorType, status, service, tokenModel },
|
|
64
|
+
services: `label_values(${calls}, ${service})`,
|
|
65
|
+
models: `label_values(${calls}, ${model})`,
|
|
66
|
+
model: {
|
|
67
|
+
rate: sumRate(selector(calls, scope), [model]),
|
|
68
|
+
errorRatio: ratio(scope, [model]),
|
|
69
|
+
latency: quantile(q, selector(buckets, scope), [model]),
|
|
70
|
+
},
|
|
71
|
+
tool: {
|
|
72
|
+
rate: sumRate(selector(calls, toolScope), [tool]),
|
|
73
|
+
errorRatio: ratio(toolScope, [tool]),
|
|
74
|
+
latency: quantile(q, selector(buckets, toolScope), [tool]),
|
|
75
|
+
},
|
|
76
|
+
errorsByType: sumRate(selector(calls, [...scope, err]), [errorType]),
|
|
77
|
+
tokens: {
|
|
78
|
+
inputRate: sumRate(selector(m.inputTokens.prometheus, tokens), [tokenModel]),
|
|
79
|
+
outputRate: sumRate(selector(m.outputTokens.prometheus, tokens), [tokenModel]),
|
|
80
|
+
inputTotal: `sum(increase(${selector(m.inputTokens.prometheus, tokens)}[$__range]))`,
|
|
81
|
+
outputTotal: `sum(increase(${selector(m.outputTokens.prometheus, tokens)}[$__range]))`,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ── From GenAiRules ─────────────────────────────────────────────
|
|
86
|
+
/** The labels a `$service` picker can read, in the order they're preferred. */
|
|
87
|
+
const SERVICE_LABELS = ["service_name", "job"];
|
|
88
|
+
const SECONDS_PER_HOUR = 3600;
|
|
89
|
+
function isRuleMetrics(x) {
|
|
90
|
+
return typeof x === "object" && x !== null && "requests" in x && "modelLabels" in x && "tokenLabels" in x && "latency" in x;
|
|
91
|
+
}
|
|
92
|
+
function ruleMetricsOf(rules) {
|
|
93
|
+
if (isRuleMetrics(rules))
|
|
94
|
+
return rules;
|
|
95
|
+
try {
|
|
96
|
+
return genAiRuleMetrics(rules);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new Error("AgentDashboard: rules must be a GenAiRules(...) result, its rule group, or genAiRuleMetrics(...) from the prometheus lexicon");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** `sum by (labels) (sel)`, or `sum (sel)` with no labels. */
|
|
103
|
+
function sumOf(sel, by) {
|
|
104
|
+
return `sum${by.length ? ` by (${by.join(", ")})` : ""} (${sel})`;
|
|
105
|
+
}
|
|
106
|
+
/** Errors over requests, both already per-second series, padded to 0 like `errorRatio`. */
|
|
107
|
+
function recordedRatio(errorSel, allSel, by) {
|
|
108
|
+
const all = sumOf(allSel, by);
|
|
109
|
+
return `(\n${sumOf(errorSel, by)}\nor\n${all} * 0\n)\n/\n${all}`;
|
|
110
|
+
}
|
|
111
|
+
/** A recorded per-second rate summed over the dashboard's range: its average over the range times the range's seconds. */
|
|
112
|
+
function overRange(sel) {
|
|
113
|
+
return `sum(avg_over_time(${sel}[$__range])) * $__range_s`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The PromQL the rules-mode agent dashboard runs, from `genAiRuleMetrics()`.
|
|
117
|
+
* Exposed for tests and for panels of your own.
|
|
118
|
+
*
|
|
119
|
+
* Every series is a recorded one, so nothing here takes a `rate`. Cost is
|
|
120
|
+
* one query per currency: amounts in two currencies are never added.
|
|
121
|
+
*/
|
|
122
|
+
export function agentRuleQueries(m, q = 0.95) {
|
|
123
|
+
const quantileValue = num(q);
|
|
124
|
+
if (!m.latency.quantiles.includes(quantileValue)) {
|
|
125
|
+
throw new Error(`AgentDashboard: the rules record latency at ${m.latency.quantiles.join(", ")}, not ${quantileValue}`);
|
|
126
|
+
}
|
|
127
|
+
const l = m.labels;
|
|
128
|
+
const service = SERVICE_LABELS.find((x) => m.modelLabels.includes(x) && m.tokenLabels.includes(x));
|
|
129
|
+
const provider = l.provider;
|
|
130
|
+
const tokensHaveProvider = provider !== undefined && m.tokenLabels.includes(provider);
|
|
131
|
+
// Cost series carry the price's provider as a rule label, whatever the token series have.
|
|
132
|
+
const costProvider = prometheusLabel(GENAI_ATTRIBUTES.providerName);
|
|
133
|
+
const svc = service ? [[service, "=~", "$service"]] : [];
|
|
134
|
+
const prov = provider ? [[provider, "=~", "$provider"]] : [];
|
|
135
|
+
const modelMatch = [l.model, "=~", "$model"];
|
|
136
|
+
const scope = [...svc, ...prov, modelMatch];
|
|
137
|
+
const tokenScope = [...svc, ...(tokensHaveProvider ? prov : []), modelMatch];
|
|
138
|
+
const modelBy = [...(provider ? [provider] : []), l.model, l.operation];
|
|
139
|
+
const tokenBy = [...(tokensHaveProvider ? [provider] : []), l.model];
|
|
140
|
+
const tokens = (type) => selector(m.tokens, [...tokenScope, [l.tokenType, "=", type]]);
|
|
141
|
+
const requests = selector(m.requests, scope);
|
|
142
|
+
const errors = selector(m.errors, scope);
|
|
143
|
+
return {
|
|
144
|
+
labels: { ...(service ? { service } : {}), ...l, modelBy, tokenBy },
|
|
145
|
+
variables: {
|
|
146
|
+
...(service ? { services: `label_values(${m.requests}, ${service})` } : {}),
|
|
147
|
+
...(provider ? { providers: `label_values(${selector(m.requests, svc)}, ${provider})` } : {}),
|
|
148
|
+
models: `label_values(${selector(m.requests, [...svc, ...prov])}, ${l.model})`,
|
|
149
|
+
},
|
|
150
|
+
model: {
|
|
151
|
+
rate: sumOf(requests, modelBy),
|
|
152
|
+
errorRatio: recordedRatio(errors, requests, modelBy),
|
|
153
|
+
latency: selector(m.latency.record, [...scope, [l.quantile, "=", quantileValue]]),
|
|
154
|
+
},
|
|
155
|
+
...(provider
|
|
156
|
+
? {
|
|
157
|
+
provider: {
|
|
158
|
+
rate: sumOf(requests, [provider]),
|
|
159
|
+
errorRatio: recordedRatio(errors, requests, [provider]),
|
|
160
|
+
...(tokensHaveProvider ? { tokens: sumOf(selector(m.tokens, tokenScope), [provider, l.tokenType]) } : {}),
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
: {}),
|
|
164
|
+
...(m.tool && l.tool
|
|
165
|
+
? {
|
|
166
|
+
tool: {
|
|
167
|
+
rate: sumOf(selector(m.tool.calls, svc), [l.tool]),
|
|
168
|
+
errorRatio: recordedRatio(selector(m.tool.errors, svc), selector(m.tool.calls, svc), [l.tool]),
|
|
169
|
+
latency: selector(m.tool.latency.record, [...svc, [l.quantile, "=", m.tool.latency.quantiles[0]]]),
|
|
170
|
+
quantile: Number(m.tool.latency.quantiles[0]),
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
174
|
+
errorsByType: sumOf(errors, [l.errorType]),
|
|
175
|
+
tokens: {
|
|
176
|
+
inputRate: sumOf(tokens(GENAI_TOKEN_TYPES.input), tokenBy),
|
|
177
|
+
outputRate: sumOf(tokens(GENAI_TOKEN_TYPES.output), tokenBy),
|
|
178
|
+
inputTotal: overRange(tokens(GENAI_TOKEN_TYPES.input)),
|
|
179
|
+
outputTotal: overRange(tokens(GENAI_TOKEN_TYPES.output)),
|
|
180
|
+
},
|
|
181
|
+
cost: m.cost
|
|
182
|
+
? m.currencies.map((currency) => {
|
|
183
|
+
const sel = selector(m.cost, [...svc, ...prov, modelMatch, [l.currency, "=", currency]]);
|
|
184
|
+
return {
|
|
185
|
+
currency,
|
|
186
|
+
perHour: `${sumOf(sel, [costProvider, l.model])} * ${SECONDS_PER_HOUR}`,
|
|
187
|
+
total: overRange(sel),
|
|
188
|
+
};
|
|
189
|
+
})
|
|
190
|
+
: [],
|
|
191
|
+
...(m.alerts.length > 0
|
|
192
|
+
? {
|
|
193
|
+
alertsFiring: `sum(${selector("ALERTS", [
|
|
194
|
+
["alertname", "=~", [...new Set(m.alerts.map((a) => a.alert))].join("|")],
|
|
195
|
+
["alertstate", "=", "firing"],
|
|
196
|
+
])}) or vector(0)`,
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/** Grafana's unit for an amount in `currency`, or plain numbers for one it has no unit for. */
|
|
202
|
+
function currencyUnit(currency) {
|
|
203
|
+
const id = `currency${currency}`;
|
|
204
|
+
return GRAFANA_UNIT_IDS.includes(id) ? id : "short";
|
|
205
|
+
}
|
|
206
|
+
/** `prices as of 2026-09-29`, or a span of dates when the prices were read on different days. */
|
|
207
|
+
function asOfText(dates) {
|
|
208
|
+
const sorted = [...new Set(dates)].sort();
|
|
209
|
+
if (sorted.length === 0)
|
|
210
|
+
return undefined;
|
|
211
|
+
return sorted.length === 1 ? `prices as of ${sorted[0]}` : `prices as of ${sorted[0]} to ${sorted[sorted.length - 1]}`;
|
|
212
|
+
}
|
|
213
|
+
function rulesDashboard(props, m) {
|
|
214
|
+
const qv = props.quantile ?? 0.95;
|
|
215
|
+
if (!(typeof qv === "number" && qv > 0 && qv < 1))
|
|
216
|
+
throw new Error(`AgentDashboard: quantile must be between 0 and 1, got ${String(qv)}`);
|
|
217
|
+
const q = agentRuleQueries(m, qv);
|
|
218
|
+
const ds = props.datasource;
|
|
219
|
+
const p = quantileName(qv);
|
|
220
|
+
const { labels } = q;
|
|
221
|
+
const variable = (name, label, query) => new QueryVariable({ name, label, datasource: ds, query, multi: true, includeAll: true, allValue: ".*", refresh: "onTimeRangeChange", sort: 1 });
|
|
222
|
+
const variables = [
|
|
223
|
+
...(q.variables.services ? [variable("service", "Service", q.variables.services)] : []),
|
|
224
|
+
...(q.variables.providers ? [variable("provider", "Provider", q.variables.providers)] : []),
|
|
225
|
+
variable("model", "Model", q.variables.models),
|
|
226
|
+
];
|
|
227
|
+
const series = (title, expr, by, unit, description, w = 8) => new TimeSeriesPanel({
|
|
228
|
+
title,
|
|
229
|
+
description,
|
|
230
|
+
datasource: ds,
|
|
231
|
+
gridPos: { w, h: 8 },
|
|
232
|
+
targets: [new PromQuery({ expr, legendFormat: legend(...by) })],
|
|
233
|
+
fieldConfig: { defaults: { unit, ...(unit === "percentunit" ? { min: 0 } : {}) } },
|
|
234
|
+
});
|
|
235
|
+
// One instant query at the end of the range, as in the span-metrics mode.
|
|
236
|
+
const stat = (title, expr, unit, description, w = 4, legendFormat = title) => new StatPanel({
|
|
237
|
+
title,
|
|
238
|
+
description,
|
|
239
|
+
datasource: ds,
|
|
240
|
+
gridPos: { w, h: 8 },
|
|
241
|
+
targets: [new PromQuery({ expr, legendFormat, instant: true, range: false })],
|
|
242
|
+
options: { graphMode: "none", colorMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
|
|
243
|
+
fieldConfig: { defaults: { unit, decimals: unit === "short" ? 0 : 2 } },
|
|
244
|
+
});
|
|
245
|
+
const withService = (by) => (labels.service ? [...by, labels.service] : by);
|
|
246
|
+
const rows = [];
|
|
247
|
+
rows.push(new Row({
|
|
248
|
+
title: "Models",
|
|
249
|
+
panels: [
|
|
250
|
+
series("Requests by model", q.model.rate, labels.modelBy, "reqps", `Model requests per second, from ${m.requests}.`),
|
|
251
|
+
series("Errors by model", q.model.errorRatio, labels.modelBy, "percentunit", `Share of model requests that failed, from ${m.errors} over ${m.requests}.`),
|
|
252
|
+
series(`Latency ${p} by model`, q.model.latency, withService(labels.modelBy), "s", `Recorded per ${m.modelLabels.join(", ")} in ${m.latency.record}; a quantile can't be summed, so each of those is its own line.`),
|
|
253
|
+
],
|
|
254
|
+
}));
|
|
255
|
+
if (q.provider && labels.provider) {
|
|
256
|
+
rows.push(new Row({
|
|
257
|
+
title: "Providers",
|
|
258
|
+
panels: [
|
|
259
|
+
series("Requests by provider", q.provider.rate, [labels.provider], "reqps", `Model requests per second by ${GENAI_ATTRIBUTES.providerName}.`),
|
|
260
|
+
series("Errors by provider", q.provider.errorRatio, [labels.provider], "percentunit", "Share of model requests that failed, by provider."),
|
|
261
|
+
...(q.provider.tokens
|
|
262
|
+
? [series("Tokens by provider", q.provider.tokens, [labels.provider, labels.tokenType], "short", `Tokens per second by provider and ${GENAI_ATTRIBUTES.tokenType}.`)]
|
|
263
|
+
: []),
|
|
264
|
+
],
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
if (q.tool && labels.tool) {
|
|
268
|
+
rows.push(new Row({
|
|
269
|
+
title: "Tools",
|
|
270
|
+
panels: [
|
|
271
|
+
series("Tool calls", q.tool.rate, [labels.tool], "reqps", `Calls per tool per second, from ${m.tool.calls}.`),
|
|
272
|
+
series("Errors by tool", q.tool.errorRatio, [labels.tool], "percentunit", `Share of tool calls that failed, from ${m.tool.errors} over ${m.tool.calls}.`),
|
|
273
|
+
series(`Latency ${quantileName(q.tool.quantile)} by tool`, q.tool.latency, withService([labels.tool]), "s", `From ${m.tool.latency.record}.`),
|
|
274
|
+
],
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
rows.push(new Row({
|
|
278
|
+
title: "Errors",
|
|
279
|
+
panels: [
|
|
280
|
+
series("Errors by type", q.errorsByType, [labels.errorType], "reqps", `Failed model requests per second by ${GENAI_ATTRIBUTES.errorType}, from ${m.errors}.`, q.alertsFiring ? 20 : 24),
|
|
281
|
+
...(q.alertsFiring
|
|
282
|
+
? [
|
|
283
|
+
stat("GenAI alerts firing", q.alertsFiring, "short", `Firing alerts of the ${m.group} rule group: ${[...new Set(m.alerts.map((a) => a.alert))].join(", ")}.`),
|
|
284
|
+
]
|
|
285
|
+
: []),
|
|
286
|
+
],
|
|
287
|
+
}));
|
|
288
|
+
const tokenNote = `Over the dashboard's time range, from ${m.tokens}.`;
|
|
289
|
+
rows.push(new Row({
|
|
290
|
+
title: "Tokens",
|
|
291
|
+
panels: [
|
|
292
|
+
series("Input tokens by model", q.tokens.inputRate, labels.tokenBy, "short", `Input tokens per second by model, from ${m.tokens}.`),
|
|
293
|
+
series("Output tokens by model", q.tokens.outputRate, labels.tokenBy, "short", `Output tokens per second by model, from ${m.tokens}.`),
|
|
294
|
+
stat("Input tokens", q.tokens.inputTotal, "short", tokenNote),
|
|
295
|
+
stat("Output tokens", q.tokens.outputTotal, "short", tokenNote),
|
|
296
|
+
],
|
|
297
|
+
}));
|
|
298
|
+
if (q.cost.length > 0) {
|
|
299
|
+
const panels = q.cost.flatMap((c) => {
|
|
300
|
+
const prices = m.prices.filter((x) => x.currency === c.currency);
|
|
301
|
+
const asOf = asOfText(prices.flatMap((x) => (x.asOf ? [x.asOf] : [])));
|
|
302
|
+
const note = `From ${m.cost} and the declared prices in ${c.currency}${asOf ? `, ${asOf}` : ""} (${[...new Set(prices.map((x) => x.source))].join(", ")}). ` +
|
|
303
|
+
`Models without a price are not counted, and amounts in other currencies are shown apart, never added.`;
|
|
304
|
+
const unit = currencyUnit(c.currency);
|
|
305
|
+
return [
|
|
306
|
+
series(`Spend per hour in ${c.currency}`, c.perHour, [prometheusLabel(GENAI_ATTRIBUTES.providerName), labels.model], unit, note, 16),
|
|
307
|
+
stat(`Spend in ${c.currency}${asOf ? `, ${asOf}` : ""}`, c.total, unit, `Over the dashboard's time range. ${note}`, 8, c.currency),
|
|
308
|
+
];
|
|
309
|
+
});
|
|
310
|
+
rows.push(new Row({ title: "Cost", panels }));
|
|
311
|
+
}
|
|
312
|
+
const noService = labels.service ? "" : " Build the rules with groupBy service_name or job for a service picker.";
|
|
313
|
+
return new Dashboard({
|
|
314
|
+
...dashboardProps(props, {
|
|
315
|
+
title: q.cost.length > 0 ? "Agents: models, tools, tokens and cost" : "Agents: models, tools and tokens",
|
|
316
|
+
uid: slugUid(`${m.group}-agents`),
|
|
317
|
+
description: `Requests, errors and latency per model${labels.provider ? " and provider" : ""}${q.tool ? " and tool" : ""}, tokens${q.cost.length > 0 ? " and cost" : ""}, from the ${m.group} GenAI recording rules (${m.source} metrics).${noService}`,
|
|
318
|
+
tags: ["genai", "agents"],
|
|
319
|
+
}),
|
|
320
|
+
variables,
|
|
321
|
+
panels: rows,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* A dashboard for agents instrumented with GenAI spans: calls, errors and
|
|
326
|
+
* latency per model and per tool, errors by type, and token usage per
|
|
327
|
+
* model, from the GenAI preset's metrics.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* ```ts
|
|
331
|
+
* import { genAiMetrics } from "@intentius/chant-lexicon-otel";
|
|
332
|
+
* import { AgentDashboard } from "@intentius/chant-lexicon-grafana";
|
|
333
|
+
*
|
|
334
|
+
* export const agents = AgentDashboard({ genAi: genAiMetrics(), datasource: prometheus });
|
|
335
|
+
*
|
|
336
|
+
* // Or from the prometheus lexicon's GenAiRules, with provider, cost and alerts:
|
|
337
|
+
* export const genai = GenAiRules({ genAi: genAiMetrics({ clientMetrics: "derive" }), prices, groupBy: ["service_name"] });
|
|
338
|
+
* export const agentRules = AgentDashboard({ rules: genai, datasource: prometheus });
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export const AgentDashboard = Composite((props) => {
|
|
342
|
+
requireDatasource("AgentDashboard", props.datasource);
|
|
343
|
+
if (props.rules !== undefined) {
|
|
344
|
+
if (props.genAi !== undefined)
|
|
345
|
+
throw new Error("AgentDashboard: give genAi or rules, not both; the rules already read the preset's metrics");
|
|
346
|
+
return { dashboard: rulesDashboard(props, ruleMetricsOf(props.rules)) };
|
|
347
|
+
}
|
|
348
|
+
if (!props.genAi)
|
|
349
|
+
throw new Error("AgentDashboard: genAi is required (genAiMetrics(...) from the otel lexicon)");
|
|
350
|
+
const m = metricsOf(props.genAi);
|
|
351
|
+
const qv = props.quantile ?? 0.95;
|
|
352
|
+
if (!(typeof qv === "number" && qv > 0 && qv < 1))
|
|
353
|
+
throw new Error(`AgentDashboard: quantile must be between 0 and 1, got ${String(qv)}`);
|
|
354
|
+
const q = agentQueries(m, qv);
|
|
355
|
+
const ds = props.datasource;
|
|
356
|
+
const p = quantileName(qv);
|
|
357
|
+
const latencyUnit = durationUnit(m.duration.unit);
|
|
358
|
+
const prefix = m.calls.name.replace(/\.calls$/, "");
|
|
359
|
+
const service = new QueryVariable({
|
|
360
|
+
name: "service",
|
|
361
|
+
label: "Service",
|
|
362
|
+
datasource: ds,
|
|
363
|
+
query: q.services,
|
|
364
|
+
multi: true,
|
|
365
|
+
includeAll: true,
|
|
366
|
+
allValue: ".*",
|
|
367
|
+
refresh: "onTimeRangeChange",
|
|
368
|
+
sort: 1,
|
|
369
|
+
});
|
|
370
|
+
const model = new QueryVariable({
|
|
371
|
+
name: "model",
|
|
372
|
+
label: "Model",
|
|
373
|
+
datasource: ds,
|
|
374
|
+
query: q.models,
|
|
375
|
+
multi: true,
|
|
376
|
+
includeAll: true,
|
|
377
|
+
allValue: ".*",
|
|
378
|
+
refresh: "onTimeRangeChange",
|
|
379
|
+
sort: 1,
|
|
380
|
+
});
|
|
381
|
+
const series = (title, expr, by, unit, description) => new TimeSeriesPanel({
|
|
382
|
+
title,
|
|
383
|
+
...(description ? { description } : {}),
|
|
384
|
+
datasource: ds,
|
|
385
|
+
gridPos: { w: 8, h: 8 },
|
|
386
|
+
targets: [new PromQuery({ expr, legendFormat: legend(by) })],
|
|
387
|
+
fieldConfig: { defaults: { unit, ...(unit === "percentunit" ? { min: 0 } : {}) } },
|
|
388
|
+
});
|
|
389
|
+
const { labels } = q;
|
|
390
|
+
const models = new Row({
|
|
391
|
+
title: "Models",
|
|
392
|
+
panels: [
|
|
393
|
+
series("Calls by model", q.model.rate, labels.model, "reqps", `GenAI operations per second, from ${m.calls.prometheus}.`),
|
|
394
|
+
series("Errors by model", q.model.errorRatio, labels.model, "percentunit", `Share of operations that ended with ${labels.status}="${SPAN_STATUS_ERROR}".`),
|
|
395
|
+
series(`Latency ${p} by model`, q.model.latency, labels.model, latencyUnit, `From ${m.duration.prometheus}.`),
|
|
396
|
+
],
|
|
397
|
+
});
|
|
398
|
+
const tools = new Row({
|
|
399
|
+
title: "Tools",
|
|
400
|
+
panels: [
|
|
401
|
+
series("Tool calls", q.tool.rate, labels.tool, "reqps", `Operations with a ${GENAI_ATTRIBUTES.toolName}, per second.`),
|
|
402
|
+
series("Errors by tool", q.tool.errorRatio, labels.tool, "percentunit"),
|
|
403
|
+
series(`Latency ${p} by tool`, q.tool.latency, labels.tool, latencyUnit),
|
|
404
|
+
],
|
|
405
|
+
});
|
|
406
|
+
const errors = new Row({
|
|
407
|
+
title: "Errors",
|
|
408
|
+
panels: [
|
|
409
|
+
new TimeSeriesPanel({
|
|
410
|
+
title: "Errors by type",
|
|
411
|
+
description: `Failed operations per second by ${GENAI_ATTRIBUTES.errorType}.`,
|
|
412
|
+
datasource: ds,
|
|
413
|
+
gridPos: { w: 24, h: 8 },
|
|
414
|
+
targets: [new PromQuery({ expr: q.errorsByType, legendFormat: legend(labels.errorType) })],
|
|
415
|
+
fieldConfig: { defaults: { unit: "reqps" } },
|
|
416
|
+
}),
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
// One instant query at the end of the range: `increase(...[$__range])`
|
|
420
|
+
// is the total over the whole range, so evaluating it at every step of a
|
|
421
|
+
// range query would only repeat that work for points the stat throws away.
|
|
422
|
+
const tokenStat = (title, expr) => new StatPanel({
|
|
423
|
+
title,
|
|
424
|
+
description: "Over the dashboard's time range.",
|
|
425
|
+
datasource: ds,
|
|
426
|
+
gridPos: { w: 4, h: 8 },
|
|
427
|
+
targets: [new PromQuery({ expr, legendFormat: title, instant: true, range: false })],
|
|
428
|
+
options: { graphMode: "none", colorMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
|
|
429
|
+
fieldConfig: { defaults: { unit: "short", decimals: 0 } },
|
|
430
|
+
});
|
|
431
|
+
const tokenRate = (title, expr, source) => new TimeSeriesPanel({
|
|
432
|
+
title,
|
|
433
|
+
description: `Tokens per second by model, from ${source}.`,
|
|
434
|
+
datasource: ds,
|
|
435
|
+
gridPos: { w: 8, h: 8 },
|
|
436
|
+
targets: [new PromQuery({ expr, legendFormat: legend(labels.tokenModel) })],
|
|
437
|
+
fieldConfig: { defaults: { unit: "short" }, overrides: [] },
|
|
438
|
+
options: { tooltip: { mode: "multi" } },
|
|
439
|
+
});
|
|
440
|
+
const tokens = new Row({
|
|
441
|
+
title: "Tokens",
|
|
442
|
+
panels: [
|
|
443
|
+
tokenRate("Input tokens by model", q.tokens.inputRate, m.inputTokens.prometheus),
|
|
444
|
+
tokenRate("Output tokens by model", q.tokens.outputRate, m.outputTokens.prometheus),
|
|
445
|
+
tokenStat("Input tokens", q.tokens.inputTotal),
|
|
446
|
+
tokenStat("Output tokens", q.tokens.outputTotal),
|
|
447
|
+
],
|
|
448
|
+
});
|
|
449
|
+
const dashboard = new Dashboard({
|
|
450
|
+
...dashboardProps(props, {
|
|
451
|
+
title: "Agents: models, tools and tokens",
|
|
452
|
+
uid: slugUid(`${prefix}-agents`),
|
|
453
|
+
description: `Calls, errors and latency per model and tool, and token usage per model, from the ${prefix} GenAI metrics.`,
|
|
454
|
+
tags: ["genai", "agents"],
|
|
455
|
+
}),
|
|
456
|
+
variables: [service, model],
|
|
457
|
+
panels: [models, tools, errors, tokens],
|
|
458
|
+
});
|
|
459
|
+
return { dashboard };
|
|
460
|
+
}, "AgentDashboard");
|