Examples
Each example lives under lexicons/grafana/examples/ and builds with chant build src --lexicon grafana (dashboards-from-declarations also with --lexicon otel and --lexicon prometheus, alerting also with --lexicon prometheus). The lexicon’s tests build it, lint it with no warnings, run the GRAF1xx checks over the output, and validate each dashboard against the pinned Grafana schema.
getting-started
Section titled “getting-started”A service overview over Prometheus, Tempo and Loki: RED panels over span metrics, a heatmap of latency buckets, a table of slow traces from TraceQL, the service’s logs from LogQL, and a trace panel driven by a textbox variable. It uses every built-in panel type, two rows (one collapsed), a query variable, a dashboard link and a folder, and links the Tempo datasource to Loki and Prometheus through jsonData.
```typescript title="datasources.ts"/** * A service overview dashboard over Prometheus, Tempo and Loki, declared * once and built into dashboard JSON and Grafana's provisioning files. * This file: the three datasources, as the Grafana container reaches them. */import { Datasource } from "@intentius/chant-lexicon-grafana";
const loki = new Datasource({ name: "Loki", type: "loki", url: "http://loki:3100" });
const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090", isDefault: true,});
// Trace-to-logs: the declared Loki datasource is written as its uid.const tracesToLogs = { datasourceUid: loki, filterByTraceID: true, spanStartTimeShift: "-5m", spanEndTimeShift: "5m" };const tempoSettings = { tracesToLogsV2: tracesToLogs, serviceMap: { datasourceUid: prometheus } };
const tempo = new Datasource({ name: "Tempo", type: "tempo", url: "http://tempo:3200", jsonData: tempoSettings });
export { prometheus, tempo, loki };```ts title="src/queries.ts"```typescript title="queries.ts"/** * The queries, one per question. PromQL over the span metrics the * OpenTelemetry collector's spanmetrics connector produces, TraceQL for slow * traces and one trace by id, LogQL for the service's logs. */import { PromQuery, TempoQuery, LokiQuery } from "@intentius/chant-lexicon-grafana";
const calls = 'traces_span_metrics_calls_total{service_name="$service"}';const buckets = 'traces_span_metrics_duration_milliseconds_bucket{service_name="$service"}';
const serverSpans = 'service_name="$service", span_kind=~"SPAN_KIND_SERVER|SPAN_KIND_CONSUMER"';const serverCalls = `traces_span_metrics_calls_total{${serverSpans}}`;const serverErrors = `traces_span_metrics_calls_total{${serverSpans}, status_code="STATUS_CODE_ERROR"}`;const allRate = `sum(rate(${serverCalls}[$__rate_interval]))`;const errorRate = `sum(rate(${serverErrors}[$__rate_interval]))`;
const requestRate = new PromQuery({ expr: `sum by (span_name) (rate(${calls}[$__rate_interval]))`, legendFormat: "{{span_name}}",});
const errorRatio = new PromQuery({ // The shape errorRatio() in the composites builds: (errors or all * 0) / all. // The padded numerator reads 0% instead of "No data" while nothing errors. // A constructor property has to be statically evaluable, so the call is not made here. expr: `(${errorRate} or ${allRate} * 0) / ${allRate}`, instant: true,});
const latencyP95 = new PromQuery({ expr: `histogram_quantile(0.95, sum by (le) (rate(${buckets}[$__rate_interval])))`, legendFormat: "p95",});
const latencyBuckets = new PromQuery({ expr: `sum by (le) (rate(${buckets}[$__rate_interval]))`, format: "heatmap" });
const slowTraces = new TempoQuery({ query: '{ resource.service.name = "$service" && duration > 500ms }', limit: 20, tableType: "traces" });
const traceById = new TempoQuery({ query: "$traceId" });
const serviceLogs = new LokiQuery({ expr: '{service_name="$service"} |= ``' });
export { requestRate, errorRatio, latencyP95, latencyBuckets, slowTraces, traceById, serviceLogs };```ts title="src/dashboard.ts"```typescript title="dashboard.ts"/** * The dashboard: variables, a row for the service's rates, errors and * latency, and a collapsed row for digging into slow requests. */import { Dashboard, Row } from "@intentius/chant-lexicon-grafana";import { service, traceId } from "./variables";import { intro, errors, errorGauge, rate, latency } from "./overview-panels";import { latencyDistribution, slowest, logs, trace } from "./debug-panels";
const red = new Row({ title: "Rate, errors, duration", panels: [errors, errorGauge, rate, latency] });
const investigate = new Row({ title: "Investigate", collapsed: true, panels: [latencyDistribution, slowest, logs, trace] });
const lastHour = { from: "now-1h", to: "now" };const docs = { title: "Runbook", type: "link" as const, url: "https://example.com/runbooks/service", targetBlank: true };
const serviceOverview = new Dashboard({ title: "Service overview", uid: "service-overview", tags: ["chant", "red"], time: lastHour, refresh: "30s", graphTooltip: "sharedCrosshair", variables: [service, traceId], panels: [intro, red, investigate], links: [docs], folder: "Services",});
export { serviceOverview };## dashboards-from-declarations
A collector, an SLO, GenAI recording rules and four dashboards in one build root, built with `chant build src --lexicon otel`, `--lexicon prometheus` and `--lexicon grafana`. The collector turns every span into RED metrics through a `spanmetrics` connector with the namespace `shop`, and GenAI spans into per-model metrics and token sums through the GenAI preset under `agents`. The checkout SLO writes its SLI with the names `spanMetricsNames()` reads from the same connector. `RedDashboard`, `SloDashboard` and `AgentDashboard` build their dashboards from the connector, the `Slo` and the preset, so changing `namespace: "shop"` moves the SLO's rules and the RED dashboard's queries together. `GenAiRules` records per-provider series and cost from a made-up price table, and a second `AgentDashboard` reads them. See [Dashboards from Declarations](../composites/).
```ts title="src/components.ts"```typescript title="components.ts"/** * The collector's components. Every span becomes RED metrics through a * `spanmetrics` connector; GenAI spans also become per-model and per-tool * metrics and token sums through the GenAI preset; Prometheus scrapes it all * from the `prometheus` exporter, and traces go to Tempo. */import { BatchProcessor, genAiComponents, MemoryLimiterProcessor, OtlpExporter, OtlpReceiver, PrometheusExporter, SpanMetricsConnector, type OtlpReceiverConfig, type SpanMetricsConnectorConfig,} from "@intentius/chant-lexicon-otel";
const protocols: OtlpReceiverConfig["protocols"] = { grpc: { endpoint: "0.0.0.0:4317" }, http: { endpoint: "0.0.0.0:4318" } };const otlp = new OtlpReceiver({ protocols });
const memoryLimiter = new MemoryLimiterProcessor({ check_interval: "1s", limit_percentage: 80, spike_limit_percentage: 20 });const batch = new BatchProcessor({});
const plaintext = { insecure: true };const tempoTraces = new OtlpExporter({ name: "tempo", endpoint: "tempo:4317", tls: plaintext });
const milliseconds: SpanMetricsConnectorConfig["histogram"] = { unit: "ms", explicit: { buckets: ["5ms", "10ms", "25ms", "50ms", "100ms", "250ms", "500ms", "1s", "2s", "5s"] },};
/** Rename the namespace and the RED dashboard and the SLO follow. */const spans = new SpanMetricsConnector({ namespace: "shop", histogram: milliseconds, metrics_flush_interval: "15s" });
/** Served on 8889 for Prometheus to scrape. */const metricsEndpoint = new PrometheusExporter({ endpoint: "0.0.0.0:8889" });
/** The GenAI preset's pieces, its metric names under `agents`, calls and durations also by provider. */const genai = genAiComponents({ namespace: "agents", providerDimensions: true });
export { otlp, memoryLimiter, batch, tempoTraces, spans, metricsEndpoint, genai };```ts title="src/slo.ts"```typescript title="slo.ts"/** * An SLO on checkout, written over the span metrics the collector emits. * The SLI takes the metric and label names from the connector declaration, * so the SLO, its rules and its dashboard move together. */import { spanMetricsNames } from "@intentius/chant-lexicon-otel";import { Slo } from "@intentius/chant-lexicon-prometheus";import { spans } from "./components";
const names = spanMetricsNames(spans);const calls = names.calls.prometheus;const checkoutSpans = `${names.labels.spanName}="checkout"`;const failed = `${names.labels.statusCode}="${names.errorStatus}"`;
const checkout = Slo({ name: "checkout", objective: 0.999, window: "30d", description: "Checkout calls end without an error span.", sli: { errors: `sum(rate(${calls}{${checkoutSpans},${failed}}[{{window}}]))`, total: `sum(rate(${calls}{${checkoutSpans}}[{{window}}]))`, }, labels: { team: "payments" },});
export { checkout };```ts title="src/genai-rules.ts"```typescript title="genai-rules.ts"/** * Recording rules over the GenAI preset's metrics, with cost from a price * table. `groupBy: ["service_name"]` keeps the service on every recorded * series, so the dashboard that reads them can offer a service picker. * * The prices are made up for the example. The lexicon ships none: read * them from each provider's pricing page and record where and when. */import { GenAiRules } from "@intentius/chant-lexicon-prometheus";import { genai } from "./components";
const genaiRules = GenAiRules({ genAi: genai, groupBy: ["service_name"], prices: [ { provider: "anthropic", model: "big", inputPerMTok: 3, outputPerMTok: 15, currency: "USD", source: "https://example.com/pricing", asOf: "2026-09-29" }, { provider: "mistral", model: "small", inputPerMTok: 1, outputPerMTok: 4, currency: "EUR", source: "https://example.com/pricing", asOf: "2026-09-29" }, ], alerts: { errorRatio: true },});
export { genaiRules };```ts title="src/dashboards.ts"```typescript title="dashboards.ts"/** * Four dashboards, each built from the declaration it reads: RED per * service from the spanmetrics connector, the checkout SLO from its `Slo`, * agent calls, errors and tokens from the GenAI preset, and the same per * provider with cost from the GenAI recording rules. */import { AgentDashboard, RedDashboard, SloDashboard } from "@intentius/chant-lexicon-grafana";import { genai, metricsEndpoint, spans } from "./components";import { prometheus } from "./datasources";import { genaiRules } from "./genai-rules";import { checkout } from "./slo";
const services = RedDashboard({ spanMetrics: spans, exporter: metricsEndpoint, datasource: prometheus, folder: "Observability" });
const checkoutSlo = SloDashboard({ slo: checkout, datasource: prometheus, folder: "Observability" });
const agents = AgentDashboard({ genAi: genai, datasource: prometheus, folder: "Observability" });
const agentCost = AgentDashboard({ rules: genaiRules, datasource: prometheus, folder: "Observability" });
export { services, checkoutSlo, agents, agentCost };## alerting
Grafana-managed alerting in one build root with the SLO it alerts on, built with `chant build src --lexicon prometheus` and `--lexicon grafana`. `SloAlertRules` turns the checkout `Slo` into four burn-rate rules that read the error ratios the `Slo`'s Prometheus rules record; two hand-written rules use a reduce and a threshold with a recovery threshold, and a Loki query. Contact points read their secrets from the environment, the policy tree pages on `severity=page` and files tickets for the rest outside weekends. `src/alerting.e2e.test.ts` provisions it into Grafana 13.2.2 and reads it back. See [Alerting](../alerting/).
```ts title="src/slo.ts"```typescript title="slo.ts"/** * An SLO on checkout requests, and its burn-rate alerts as Grafana-managed * rules. The Prometheus rule group records the error ratios; the Grafana * rules read them, with the windows and thresholds `sloMetrics()` gives. */import { Slo } from "@intentius/chant-lexicon-prometheus";import { SloAlertRules } from "@intentius/chant-lexicon-grafana";import { prometheus } from "./datasources";
const checkout = Slo({ name: "checkout", objective: 0.999, window: "30d", description: "Checkout requests answer without a 5xx.", sli: { errors: 'sum(rate(http_requests_total{job="checkout",code=~"5.."}[{{window}}]))', total: 'sum(rate(http_requests_total{job="checkout"}[{{window}}]))', }, labels: { team: "payments" },});
const checkoutBurn = SloAlertRules({ slo: checkout, datasource: prometheus, folder: "SLOs", labels: { team: "payments" }, annotations: { runbook_url: "https://runbooks.example.com/checkout-slo" },});
export { checkout, checkoutBurn };```ts title="src/rules.ts"```typescript title="rules.ts"/** * Two rules written by hand: a Prometheus latency rule built from typed * server-side expressions (reduce, and a threshold with a recovery * threshold), and a Loki rule that pages on panics. */import { AlertQuery, AlertRule, AlertRuleGroup, LokiQuery, PromQuery, ReduceExpression, ThresholdExpression,} from "@intentius/chant-lexicon-grafana";import { loki, prometheus } from "./datasources";
const p99 = new PromQuery({ datasource: prometheus, expr: 'histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{job="checkout"}[5m])))', range: true,});const p99Max = new ReduceExpression({ expression: "A", reducer: "max" });const p99Threshold = new ThresholdExpression({ expression: "B", conditions: [{ evaluator: { type: "gt", params: [0.5] }, unloadEvaluator: { type: "lt", params: [0.4] } }],});const latencyLabels = { severity: "ticket", team: "payments" };const latency = new AlertRule({ title: "Checkout p99 latency above 500ms", uid: "checkout-p99-latency", data: [p99, p99Max, p99Threshold], relativeTimeRange: { from: "30m" }, for: "10m", labels: latencyLabels, annotations: { summary: "Checkout p99 is {{ humanizeDuration $values.B.Value }}" },});
const panicsQuery = new LokiQuery({ datasource: loki, expr: 'sum(count_over_time({app="checkout"} |= "panic" [5m]))' });const panics = new AlertQuery({ query: panicsQuery, queryType: "instant", relativeTimeRange: { from: "5m" } });const panicsOverZero = new ThresholdExpression({ expression: "A", conditions: [{ evaluator: { type: "gt", params: [0] } }] });const panicLabels = { severity: "page", team: "payments" };const panicRule = new AlertRule({ title: "Checkout panicked", uid: "checkout-panics", data: [panics, panicsOverZero], noDataState: "OK", labels: panicLabels,});
const checkoutAlerts = new AlertRuleGroup({ name: "checkout", folder: "Checkout", interval: "1m", rules: [latency, panicRule] });
export { checkoutAlerts };```ts title="src/notifications.ts"```typescript title="notifications.ts"/** * Where alerts go: two contact points, a policy tree that pages on * `severity=page` and files tickets for the rest outside weekends, and the * template the email uses. Secrets come from the environment Grafana runs * in, never from this file (GRAF002). */import { ContactPoint, MuteTiming, NotificationPolicy, NotificationTemplate } from "@intentius/chant-lexicon-grafana";
const emailTemplate = new NotificationTemplate({ name: "checkout.email", template: '{{ define "checkout.email.subject" }}{{ len .Alerts.Firing }} firing: {{ .CommonLabels.alertname }}{{ end }}',});
const weekendIntervals: ConstructorParameters<typeof MuteTiming>[0]["time_intervals"] = [ { weekdays: ["saturday", "sunday"], location: "Europe/Berlin" },];const weekends = new MuteTiming({ name: "weekends", time_intervals: weekendIntervals });
const oncallReceivers: ConstructorParameters<typeof ContactPoint>[0]["receivers"] = [ { type: "slack", settings: { url: "$__env{SLACK_ONCALL_WEBHOOK}", recipient: "#checkout-oncall" } }, { type: "email", settings: { addresses: "oncall@example.com", subject: '{{ template "checkout.email.subject" . }}' } },];const oncall = new ContactPoint({ name: "oncall", receivers: oncallReceivers });
const ticketReceivers: ConstructorParameters<typeof ContactPoint>[0]["receivers"] = [ { type: "webhook", settings: { url: "https://tickets.example.com/hooks/grafana", authorization_credentials: "$__env{TICKETS_TOKEN}" } },];const tickets = new ContactPoint({ name: "tickets", receivers: ticketReceivers });
const policyGroupBy = ["grafana_folder", "alertname", "slo"];const policyRoutes: ConstructorParameters<typeof NotificationPolicy>[0]["routes"] = [ { receiver: oncall, object_matchers: [["severity", "=", "page"]], group_wait: "10s" }, { receiver: tickets, object_matchers: [["severity", "!=", "page"]], mute_time_intervals: [weekends] },];const policy = new NotificationPolicy({ receiver: tickets, group_by: policyGroupBy, repeat_interval: "4h", routes: policyRoutes });
export { emailTemplate, weekends, oncall, tickets, policy };