@intentius/chant-lexicon-grafana 0.96.0
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/README.md +50 -0
- package/dist/build.d.ts +116 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/catalog.d.ts +26 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +8 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +21 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +7 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/codegen/schema-types.d.ts +41 -0
- package/dist/codegen/schema-types.d.ts.map +1 -0
- package/dist/composites/agent-dashboard.d.ts +70 -0
- package/dist/composites/agent-dashboard.d.ts.map +1 -0
- package/dist/composites/catalog.d.ts +3 -0
- package/dist/composites/catalog.d.ts.map +1 -0
- package/dist/composites/index.d.ts +12 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/red-dashboard.d.ts +58 -0
- package/dist/composites/red-dashboard.d.ts.map +1 -0
- package/dist/composites/shared.d.ts +68 -0
- package/dist/composites/shared.d.ts.map +1 -0
- package/dist/composites/slo-dashboard.d.ts +62 -0
- package/dist/composites/slo-dashboard.d.ts.map +1 -0
- package/dist/dashboard.d.ts +84 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/datasource.d.ts +67 -0
- package/dist/datasource.d.ts.map +1 -0
- package/dist/detect.d.ts +20 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +22 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/graf101.d.ts +8 -0
- package/dist/lint/post-synth/graf101.d.ts.map +1 -0
- package/dist/lint/post-synth/graf102.d.ts +8 -0
- package/dist/lint/post-synth/graf102.d.ts.map +1 -0
- package/dist/lint/post-synth/graf103.d.ts +8 -0
- package/dist/lint/post-synth/graf103.d.ts.map +1 -0
- package/dist/lint/post-synth/graf104.d.ts +8 -0
- package/dist/lint/post-synth/graf104.d.ts.map +1 -0
- package/dist/lint/post-synth/graf105.d.ts +8 -0
- package/dist/lint/post-synth/graf105.d.ts.map +1 -0
- package/dist/lint/post-synth/graf106.d.ts +8 -0
- package/dist/lint/post-synth/graf106.d.ts.map +1 -0
- package/dist/lint/post-synth/graf107.d.ts +8 -0
- package/dist/lint/post-synth/graf107.d.ts.map +1 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts +16 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/rules/grafana-ast.d.ts +21 -0
- package/dist/lint/rules/grafana-ast.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +6 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/literal-secret.d.ts +12 -0
- package/dist/lint/rules/literal-secret.d.ts.map +1 -0
- package/dist/lint/rules/uid-syntax.d.ts +12 -0
- package/dist/lint/rules/uid-syntax.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +4 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +4 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +107 -0
- package/dist/okf/index.md +39 -0
- package/dist/okf/rules/GRAF001.md +16 -0
- package/dist/okf/rules/GRAF002.md +15 -0
- package/dist/okf/rules/GRAF101.md +15 -0
- package/dist/okf/rules/GRAF102.md +15 -0
- package/dist/okf/rules/GRAF103.md +11 -0
- package/dist/okf/rules/GRAF104.md +15 -0
- package/dist/okf/rules/GRAF105.md +11 -0
- package/dist/okf/rules/GRAF106.md +11 -0
- package/dist/okf/rules/GRAF107.md +11 -0
- package/dist/okf/types/ConstantVariable.md +9 -0
- package/dist/okf/types/CustomVariable.md +9 -0
- package/dist/okf/types/Dashboard.md +14 -0
- package/dist/okf/types/DashboardProvider.md +9 -0
- package/dist/okf/types/Datasource.md +16 -0
- package/dist/okf/types/DatasourceVariable.md +9 -0
- package/dist/okf/types/GaugePanel.md +9 -0
- package/dist/okf/types/HeatmapPanel.md +9 -0
- package/dist/okf/types/IntervalVariable.md +9 -0
- package/dist/okf/types/LogsPanel.md +9 -0
- package/dist/okf/types/LokiQuery.md +9 -0
- package/dist/okf/types/PromQuery.md +9 -0
- package/dist/okf/types/QueryVariable.md +9 -0
- package/dist/okf/types/Row.md +9 -0
- package/dist/okf/types/StatPanel.md +9 -0
- package/dist/okf/types/TablePanel.md +9 -0
- package/dist/okf/types/TempoQuery.md +9 -0
- package/dist/okf/types/TextPanel.md +9 -0
- package/dist/okf/types/TextboxVariable.md +9 -0
- package/dist/okf/types/TimeSeriesPanel.md +9 -0
- package/dist/okf/types/TracesPanel.md +9 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/panels.d.ts +138 -0
- package/dist/panels.d.ts.map +1 -0
- package/dist/pin.d.ts +32 -0
- package/dist/pin.d.ts.map +1 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/query.d.ts +81 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/rules/graf101.ts +17 -0
- package/dist/rules/graf102.ts +17 -0
- package/dist/rules/graf103.ts +17 -0
- package/dist/rules/graf104.ts +17 -0
- package/dist/rules/graf105.ts +17 -0
- package/dist/rules/graf106.ts +17 -0
- package/dist/rules/graf107.ts +17 -0
- package/dist/rules/grafana-ast.ts +70 -0
- package/dist/rules/grafana-helpers.ts +68 -0
- package/dist/rules/literal-secret.ts +48 -0
- package/dist/rules/uid-syntax.ts +56 -0
- package/dist/schema/dashboard.gen.d.ts +1231 -0
- package/dist/schema/dashboard.gen.d.ts.map +1 -0
- package/dist/schema/gauge.gen.d.ts +57 -0
- package/dist/schema/gauge.gen.d.ts.map +1 -0
- package/dist/schema/heatmap.gen.d.ts +295 -0
- package/dist/schema/heatmap.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +18 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/logs.gen.d.ts +29 -0
- package/dist/schema/logs.gen.d.ts.map +1 -0
- package/dist/schema/loki.gen.d.ts +69 -0
- package/dist/schema/loki.gen.d.ts.map +1 -0
- package/dist/schema/prometheus.gen.d.ts +169 -0
- package/dist/schema/prometheus.gen.d.ts.map +1 -0
- package/dist/schema/stat.gen.d.ts +75 -0
- package/dist/schema/stat.gen.d.ts.map +1 -0
- package/dist/schema/table.gen.d.ts +295 -0
- package/dist/schema/table.gen.d.ts.map +1 -0
- package/dist/schema/tempo.gen.d.ts +136 -0
- package/dist/schema/tempo.gen.d.ts.map +1 -0
- package/dist/schema/text.gen.d.ts +16 -0
- package/dist/schema/text.gen.d.ts.map +1 -0
- package/dist/schema/timeseries.gen.d.ts +179 -0
- package/dist/schema/timeseries.gen.d.ts.map +1 -0
- package/dist/schema-validate.d.ts +20 -0
- package/dist/schema-validate.d.ts.map +1 -0
- package/dist/serializer.d.ts +22 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skill-defs.d.ts +3 -0
- package/dist/skill-defs.d.ts.map +1 -0
- package/dist/skills/chant-grafana-provisioning.md +42 -0
- package/dist/skills/chant-grafana.md +93 -0
- package/dist/spec/fetch-cli.d.ts +3 -0
- package/dist/spec/fetch-cli.d.ts.map +1 -0
- package/dist/spec/schemas.d.ts +25 -0
- package/dist/spec/schemas.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/util.d.ts +17 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate-output.d.ts +42 -0
- package/dist/validate-output.d.ts.map +1 -0
- package/dist/validate.d.ts +12 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/variables.d.ts +100 -0
- package/dist/variables.d.ts.map +1 -0
- package/package.json +79 -0
- package/src/build.test.ts +210 -0
- package/src/build.ts +573 -0
- package/src/catalog.ts +116 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +97 -0
- package/src/codegen/generate-cli.ts +8 -0
- package/src/codegen/generate.ts +83 -0
- package/src/codegen/package.ts +41 -0
- package/src/codegen/schema-types.ts +173 -0
- package/src/composites/agent-dashboard.ts +235 -0
- package/src/composites/catalog.test.ts +37 -0
- package/src/composites/catalog.ts +223 -0
- package/src/composites/composites.test.ts +314 -0
- package/src/composites/index.ts +12 -0
- package/src/composites/red-dashboard.ts +159 -0
- package/src/composites/shared.ts +94 -0
- package/src/composites/slo-dashboard.ts +177 -0
- package/src/dashboard.ts +123 -0
- package/src/datasource.ts +93 -0
- package/src/detect.ts +30 -0
- package/src/generated/lexicon-grafana.json +107 -0
- package/src/import.e2e.test.ts +147 -0
- package/src/import.test.ts +45 -0
- package/src/index.ts +178 -0
- package/src/lint/audit-catalog.ts +87 -0
- package/src/lint/post-synth/graf101.ts +17 -0
- package/src/lint/post-synth/graf102.ts +17 -0
- package/src/lint/post-synth/graf103.ts +17 -0
- package/src/lint/post-synth/graf104.ts +17 -0
- package/src/lint/post-synth/graf105.ts +17 -0
- package/src/lint/post-synth/graf106.ts +17 -0
- package/src/lint/post-synth/graf107.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +68 -0
- package/src/lint/post-synth/index.ts +19 -0
- package/src/lint/post-synth/post-synth.test.ts +231 -0
- package/src/lint/rules/grafana-ast.ts +70 -0
- package/src/lint/rules/index.ts +9 -0
- package/src/lint/rules/literal-secret.ts +48 -0
- package/src/lint/rules/rules.test.ts +70 -0
- package/src/lint/rules/uid-syntax.ts +56 -0
- package/src/lsp/completions.test.ts +31 -0
- package/src/lsp/completions.ts +15 -0
- package/src/lsp/hover.test.ts +25 -0
- package/src/lsp/hover.ts +28 -0
- package/src/package-cli.ts +17 -0
- package/src/panels.ts +275 -0
- package/src/pin.ts +68 -0
- package/src/plugin.test.ts +50 -0
- package/src/plugin.ts +116 -0
- package/src/query.ts +160 -0
- package/src/schema/dashboard.gen.ts +1289 -0
- package/src/schema/gauge.gen.ts +64 -0
- package/src/schema/heatmap.gen.ts +321 -0
- package/src/schema/index.ts +17 -0
- package/src/schema/logs.gen.ts +34 -0
- package/src/schema/loki.gen.ts +77 -0
- package/src/schema/prometheus.gen.ts +181 -0
- package/src/schema/stat.gen.ts +86 -0
- package/src/schema/table.gen.ts +334 -0
- package/src/schema/tempo.gen.ts +146 -0
- package/src/schema/text.gen.ts +22 -0
- package/src/schema/timeseries.gen.ts +208 -0
- package/src/schema-validate.ts +143 -0
- package/src/schema.test.ts +76 -0
- package/src/serializer.test.ts +170 -0
- package/src/serializer.ts +37 -0
- package/src/skill-defs.ts +31 -0
- package/src/skills/chant-grafana-provisioning.md +42 -0
- package/src/skills/chant-grafana.md +93 -0
- package/src/spec/fetch-cli.ts +27 -0
- package/src/spec/schemas/dashboard.jsonschema.json +1901 -0
- package/src/spec/schemas/gauge.jsonschema.json +114 -0
- package/src/spec/schemas/heatmap.jsonschema.json +480 -0
- package/src/spec/schemas/logs.jsonschema.json +94 -0
- package/src/spec/schemas/loki.jsonschema.json +118 -0
- package/src/spec/schemas/prometheus.jsonschema.json +276 -0
- package/src/spec/schemas/stat.jsonschema.json +163 -0
- package/src/spec/schemas/table.jsonschema.json +751 -0
- package/src/spec/schemas/tempo.jsonschema.json +225 -0
- package/src/spec/schemas/text.jsonschema.json +72 -0
- package/src/spec/schemas/timeseries.jsonschema.json +471 -0
- package/src/spec/schemas.ts +56 -0
- package/src/util.ts +43 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate-output.ts +380 -0
- package/src/validate.ts +108 -0
- package/src/variables.ts +161 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RedDashboard`: rate, errors and duration per service, from a
|
|
3
|
+
* `spanmetrics` connector declaration.
|
|
4
|
+
*
|
|
5
|
+
* The metric names come from the connector's config through the otel
|
|
6
|
+
* lexicon's `spanMetricsNames()`: its namespace, its histogram unit, the
|
|
7
|
+
* default dimensions it keeps, and optionally the `prometheus` exporter's
|
|
8
|
+
* namespace. Renaming the connector's namespace moves every query here.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
12
|
+
import { spanMetricsNames, type PrometheusNaming, type SpanMetricsNames } from "@intentius/chant-lexicon-otel/metric-names";
|
|
13
|
+
import type { OTelComponent } from "@intentius/chant-lexicon-otel/define";
|
|
14
|
+
import type { SpanMetricsConnectorConfig } from "@intentius/chant-lexicon-otel/components/connectors";
|
|
15
|
+
import type { PrometheusExporterConfig } from "@intentius/chant-lexicon-otel/components/exporters";
|
|
16
|
+
import { Dashboard, type DashboardEntity } from "../dashboard";
|
|
17
|
+
import { Row, TimeSeriesPanel } from "../panels";
|
|
18
|
+
import { PromQuery } from "../query";
|
|
19
|
+
import { QueryVariable } from "../variables";
|
|
20
|
+
import { slugUid } from "../util";
|
|
21
|
+
import {
|
|
22
|
+
dashboardProps,
|
|
23
|
+
durationUnit,
|
|
24
|
+
legend,
|
|
25
|
+
quantile,
|
|
26
|
+
quantileName,
|
|
27
|
+
requireDatasource,
|
|
28
|
+
selector,
|
|
29
|
+
sumRate,
|
|
30
|
+
type DashboardOptions,
|
|
31
|
+
type Matcher,
|
|
32
|
+
} from "./shared";
|
|
33
|
+
|
|
34
|
+
export interface RedDashboardProps extends DashboardOptions {
|
|
35
|
+
/**
|
|
36
|
+
* The `spanmetrics` connector the metrics come from, or the names
|
|
37
|
+
* `spanMetricsNames()` returned for it.
|
|
38
|
+
*/
|
|
39
|
+
spanMetrics: OTelComponent<"connector", "spanmetrics", SpanMetricsConnectorConfig> | SpanMetricsNames;
|
|
40
|
+
/** The `prometheus` exporter that serves them, when its `namespace` or `add_metric_suffixes` changes the names. */
|
|
41
|
+
exporter?: OTelComponent<"exporter", "prometheus", PrometheusExporterConfig> | PrometheusNaming;
|
|
42
|
+
/** Duration quantiles, one panel each (default p50, p95 and p99). */
|
|
43
|
+
quantiles?: number[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type RedDashboardMembers = { dashboard: DashboardEntity };
|
|
47
|
+
|
|
48
|
+
/** What `RedDashboard(...)` returns: its dashboard, as `dashboard`. */
|
|
49
|
+
export type RedDashboardInstance = CompositeInstance<RedDashboardMembers> & RedDashboardMembers;
|
|
50
|
+
|
|
51
|
+
const DEFAULT_QUANTILES = [0.5, 0.95, 0.99];
|
|
52
|
+
|
|
53
|
+
function isNames(x: unknown): x is SpanMetricsNames {
|
|
54
|
+
return typeof x === "object" && x !== null && "calls" in x && "labels" in x && "errorStatus" in x;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The PromQL the RED dashboard runs, from the connector's names. Exposed for tests and for panels of your own. */
|
|
58
|
+
export function redQueries(names: SpanMetricsNames, quantiles: number[] = DEFAULT_QUANTILES, serviceFilter = "$service") {
|
|
59
|
+
const svc = names.labels.service;
|
|
60
|
+
const status = names.labels.statusCode;
|
|
61
|
+
if (!svc) throw new Error("RedDashboard: the connector excludes service.name, so there is no service to break the metrics down by");
|
|
62
|
+
if (!status) throw new Error("RedDashboard: the connector excludes status.code, so errors can't be told from successes");
|
|
63
|
+
const bySvc: Matcher[] = [[svc, "=~", serviceFilter]];
|
|
64
|
+
const calls = selector(names.calls.prometheus, bySvc);
|
|
65
|
+
const errors = selector(names.calls.prometheus, [...bySvc, [status, "=", names.errorStatus]]);
|
|
66
|
+
const buckets = names.duration ? selector(`${names.duration.prometheus}_bucket`, bySvc) : undefined;
|
|
67
|
+
return {
|
|
68
|
+
rate: sumRate(calls, [svc]),
|
|
69
|
+
errorRatio: `${sumRate(errors, [svc])}\n/\n${sumRate(calls, [svc])}`,
|
|
70
|
+
duration: buckets ? quantiles.map((q) => ({ quantile: q, expr: quantile(q, buckets, [svc]) })) : [],
|
|
71
|
+
services: `label_values(${names.calls.prometheus}, ${svc})`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A RED dashboard (rate, errors, duration) per service, built from a
|
|
77
|
+
* `spanmetrics` connector declaration.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import { SpanMetricsConnector } from "@intentius/chant-lexicon-otel";
|
|
82
|
+
* import { Datasource, RedDashboard } from "@intentius/chant-lexicon-grafana";
|
|
83
|
+
*
|
|
84
|
+
* export const spans = new SpanMetricsConnector({ namespace: "shop", histogram: { unit: "s" } });
|
|
85
|
+
* export const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
|
|
86
|
+
* export const red = RedDashboard({ spanMetrics: spans, datasource: prometheus });
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export const RedDashboard = Composite<RedDashboardProps, RedDashboardMembers>((props) => {
|
|
90
|
+
requireDatasource("RedDashboard", props.datasource);
|
|
91
|
+
if (!props.spanMetrics) throw new Error("RedDashboard: spanMetrics is required (a spanmetrics connector)");
|
|
92
|
+
const names = isNames(props.spanMetrics) ? props.spanMetrics : spanMetricsNames(props.spanMetrics, props.exporter);
|
|
93
|
+
const quantiles = props.quantiles ?? DEFAULT_QUANTILES;
|
|
94
|
+
for (const q of quantiles) {
|
|
95
|
+
if (!(typeof q === "number" && q > 0 && q < 1)) throw new Error(`RedDashboard: a quantile must be between 0 and 1, got ${String(q)}`);
|
|
96
|
+
}
|
|
97
|
+
const q = redQueries(names, quantiles);
|
|
98
|
+
const svc = names.labels.service!;
|
|
99
|
+
const ds = props.datasource;
|
|
100
|
+
|
|
101
|
+
const service = new QueryVariable({
|
|
102
|
+
name: "service",
|
|
103
|
+
label: "Service",
|
|
104
|
+
datasource: ds,
|
|
105
|
+
query: q.services,
|
|
106
|
+
multi: true,
|
|
107
|
+
includeAll: true,
|
|
108
|
+
allValue: ".*",
|
|
109
|
+
refresh: "onTimeRangeChange",
|
|
110
|
+
sort: 1,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const rate = new TimeSeriesPanel({
|
|
114
|
+
title: "Rate",
|
|
115
|
+
description: `Spans per second by service, from ${names.calls.prometheus}.`,
|
|
116
|
+
datasource: ds,
|
|
117
|
+
gridPos: { w: 12, h: 8 },
|
|
118
|
+
targets: [new PromQuery({ expr: q.rate, legendFormat: legend(svc) })],
|
|
119
|
+
fieldConfig: { defaults: { unit: "reqps" } },
|
|
120
|
+
});
|
|
121
|
+
const errors = new TimeSeriesPanel({
|
|
122
|
+
title: "Errors",
|
|
123
|
+
description: `Share of spans with ${names.labels.statusCode}="${names.errorStatus}", by service.`,
|
|
124
|
+
datasource: ds,
|
|
125
|
+
gridPos: { w: 12, h: 8 },
|
|
126
|
+
targets: [new PromQuery({ expr: q.errorRatio, legendFormat: legend(svc) })],
|
|
127
|
+
fieldConfig: { defaults: { unit: "percentunit", min: 0 } },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const rows = [new Row({ title: "Rate and errors", panels: [rate, errors] })];
|
|
131
|
+
if (names.duration && q.duration.length > 0) {
|
|
132
|
+
const width = Math.max(6, Math.floor(24 / q.duration.length));
|
|
133
|
+
const unit = durationUnit(names.duration.unit);
|
|
134
|
+
const panels = q.duration.map(
|
|
135
|
+
(d) =>
|
|
136
|
+
new TimeSeriesPanel({
|
|
137
|
+
title: `Duration ${quantileName(d.quantile)}`,
|
|
138
|
+
description: `${quantileName(d.quantile)} span duration by service, from ${names.duration!.prometheus}.`,
|
|
139
|
+
datasource: ds,
|
|
140
|
+
gridPos: { w: width, h: 8 },
|
|
141
|
+
targets: [new PromQuery({ expr: d.expr, legendFormat: legend(svc) })],
|
|
142
|
+
fieldConfig: { defaults: { unit } },
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
rows.push(new Row({ title: "Duration", panels }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const dashboard = new Dashboard({
|
|
149
|
+
...dashboardProps(props, {
|
|
150
|
+
title: "Services: rate, errors, duration",
|
|
151
|
+
uid: slugUid(`red-${names.namespace || "spans"}`),
|
|
152
|
+
description: `Rate, errors and duration per service from the ${names.namespace || "unprefixed"} span metrics.`,
|
|
153
|
+
tags: ["red", "spanmetrics"],
|
|
154
|
+
}),
|
|
155
|
+
variables: [service],
|
|
156
|
+
panels: rows,
|
|
157
|
+
});
|
|
158
|
+
return { dashboard };
|
|
159
|
+
}, "RedDashboard");
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the dashboard composites share: the dashboard-level props they all
|
|
3
|
+
* take, a PromQL selector builder, and the field configs they reuse.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { DatasourceInput } from "../query";
|
|
7
|
+
import type { DashboardLinkInput } from "../dashboard";
|
|
8
|
+
import type { ThresholdsConfig } from "../schema/dashboard.gen";
|
|
9
|
+
|
|
10
|
+
/** Dashboard settings every composite takes; each has a default derived from what it reads. */
|
|
11
|
+
export interface DashboardOptions {
|
|
12
|
+
/** The Prometheus the metrics are queried from: a declared `Datasource`, or `{ type: "prometheus", uid }` for one declared elsewhere. */
|
|
13
|
+
datasource: DatasourceInput<"prometheus">;
|
|
14
|
+
title?: string;
|
|
15
|
+
/** The dashboard uid. Defaults to one derived from the source declaration. */
|
|
16
|
+
uid?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
tags?: string[];
|
|
19
|
+
/** The Grafana folder to provision it into. */
|
|
20
|
+
folder?: string;
|
|
21
|
+
/** Initial time range. */
|
|
22
|
+
time?: { from: string; to: string };
|
|
23
|
+
/** Auto-refresh interval, e.g. `30s`. */
|
|
24
|
+
refresh?: string;
|
|
25
|
+
links?: DashboardLinkInput[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A label matcher: `[label, op, value]`. */
|
|
29
|
+
export type Matcher = [label: string, op: "=" | "!=" | "=~" | "!~", value: string];
|
|
30
|
+
|
|
31
|
+
/** `metric{a="x", b=~"y"}`; just `metric` with no matchers. */
|
|
32
|
+
export function selector(metric: string, matchers: Matcher[]): string {
|
|
33
|
+
if (matchers.length === 0) return metric;
|
|
34
|
+
return `${metric}{${matchers.map(([l, op, v]) => `${l}${op}${JSON.stringify(v)}`).join(", ")}}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `sum by (labels) (rate(sel[range]))`, or `sum (...)` with no labels. */
|
|
38
|
+
export function sumRate(sel: string, by: string[], range = "$__rate_interval"): string {
|
|
39
|
+
return `sum${by.length ? ` by (${by.join(", ")})` : ""} (rate(${sel}[${range}]))`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `histogram_quantile(q, sum by (le, labels) (rate(bucket[range])))`. */
|
|
43
|
+
export function quantile(q: number, bucketSel: string, by: string[]): string {
|
|
44
|
+
return `histogram_quantile(${num(q)}, ${sumRate(bucketSel, ["le", ...by])})`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A number as PromQL and Grafana thresholds write it, without float noise. */
|
|
48
|
+
export function num(n: number): string {
|
|
49
|
+
return String(Number(n.toPrecision(10)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** `p95` for 0.95, `p99.9` for 0.999. */
|
|
53
|
+
export function quantileName(q: number): string {
|
|
54
|
+
return `p${num(q * 100)}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Legend text naming the label, `{{service_name}}`. */
|
|
58
|
+
export function legend(...labels: string[]): string {
|
|
59
|
+
return labels.map((l) => `{{${l}}}`).join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Grafana's unit id for a spanmetrics histogram unit (`ms` or `s`). */
|
|
63
|
+
export function durationUnit(unit: string | undefined): string {
|
|
64
|
+
return unit === "s" ? "s" : "ms";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Absolute thresholds from a base colour and `[value, colour]` steps. */
|
|
68
|
+
export function thresholds(base: string, steps: Array<[number, string]>): ThresholdsConfig {
|
|
69
|
+
return { mode: "absolute", steps: [{ value: null, color: base }, ...steps.map(([value, color]) => ({ value, color }))] };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The props a composite passes to `Dashboard`, from the options and its own defaults. */
|
|
73
|
+
export function dashboardProps(o: DashboardOptions, defaults: { title: string; uid: string; description: string; tags: string[] }) {
|
|
74
|
+
return {
|
|
75
|
+
title: o.title ?? defaults.title,
|
|
76
|
+
uid: o.uid ?? defaults.uid,
|
|
77
|
+
description: o.description ?? defaults.description,
|
|
78
|
+
tags: o.tags ?? defaults.tags,
|
|
79
|
+
time: o.time ?? { from: "now-6h", to: "now" },
|
|
80
|
+
...(o.refresh !== undefined ? { refresh: o.refresh } : { refresh: "1m" }),
|
|
81
|
+
graphTooltip: "sharedCrosshair" as const,
|
|
82
|
+
...(o.folder !== undefined ? { folder: o.folder } : {}),
|
|
83
|
+
...(o.links !== undefined ? { links: o.links } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Throw unless a Prometheus datasource was passed. */
|
|
88
|
+
export function requireDatasource(composite: string, ds: unknown): void {
|
|
89
|
+
if (ds === undefined || ds === null) throw new Error(`${composite}: datasource is required (a Prometheus Datasource or { type: "prometheus", uid })`);
|
|
90
|
+
const type = (ds as { datasourceType?: unknown; pluginType?: unknown; type?: unknown }).datasourceType ??
|
|
91
|
+
(ds as { pluginType?: unknown }).pluginType ??
|
|
92
|
+
(ds as { type?: unknown }).type;
|
|
93
|
+
if (type !== "prometheus") throw new Error(`${composite}: datasource must be a Prometheus datasource, got ${String(type)}`);
|
|
94
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SloDashboard`: one SLO's SLI, error budget and burn rates, from an `Slo`
|
|
3
|
+
* declaration.
|
|
4
|
+
*
|
|
5
|
+
* Every series name, the objective, the window and the burn-rate pairs come
|
|
6
|
+
* from the prometheus lexicon's `sloMetrics()`, which reads the same
|
|
7
|
+
* declaration the rules are built from. Renaming the SLO, changing its
|
|
8
|
+
* objective or its windows moves the panels and their threshold lines with
|
|
9
|
+
* it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
13
|
+
import { sloMetrics, type SloInstance, type SloMetrics } from "@intentius/chant-lexicon-prometheus/composites/slo";
|
|
14
|
+
import type { RuleGroupEntity } from "@intentius/chant-lexicon-prometheus/rules";
|
|
15
|
+
import { Dashboard, type DashboardEntity } from "../dashboard";
|
|
16
|
+
import { Row, StatPanel, TimeSeriesPanel } from "../panels";
|
|
17
|
+
import { PromQuery } from "../query";
|
|
18
|
+
import { slugUid } from "../util";
|
|
19
|
+
import { dashboardProps, num, requireDatasource, selector, thresholds, type DashboardOptions } from "./shared";
|
|
20
|
+
|
|
21
|
+
export interface SloDashboardProps extends DashboardOptions {
|
|
22
|
+
/** The `Slo(...)` the dashboard reads, its rule group, or what `sloMetrics()` returned for it. */
|
|
23
|
+
slo: SloInstance | RuleGroupEntity | SloMetrics;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type SloDashboardMembers = { dashboard: DashboardEntity };
|
|
27
|
+
|
|
28
|
+
/** What `SloDashboard(...)` returns: its dashboard, as `dashboard`. */
|
|
29
|
+
export type SloDashboardInstance = CompositeInstance<SloDashboardMembers> & SloDashboardMembers;
|
|
30
|
+
|
|
31
|
+
function isMetrics(x: unknown): x is SloMetrics {
|
|
32
|
+
return typeof x === "object" && x !== null && "errorBudgetRemaining" in x && "burnRates" in x && "selector" in x;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The PromQL the SLO dashboard runs, from `sloMetrics()`. Exposed for tests and for panels of your own. */
|
|
36
|
+
export function sloQueries(m: SloMetrics) {
|
|
37
|
+
const s = (series: string) => `${series}${m.selector}`;
|
|
38
|
+
const budget = num(m.errorBudget);
|
|
39
|
+
return {
|
|
40
|
+
sli: `1 - ${s(m.windowErrorRatio)}`,
|
|
41
|
+
objective: s(m.objectiveRatio),
|
|
42
|
+
errorBudgetRemaining: s(m.errorBudgetRemaining),
|
|
43
|
+
alertsFiring: `sum(${selector("ALERTS", [
|
|
44
|
+
["alertname", "=", m.alertName],
|
|
45
|
+
["slo", "=", m.name],
|
|
46
|
+
["alertstate", "=", "firing"],
|
|
47
|
+
])}) or vector(0)`,
|
|
48
|
+
burnRates: m.burnRates.map((b) => ({
|
|
49
|
+
...b,
|
|
50
|
+
longExpr: `${s(b.longRecord)} / ${budget}`,
|
|
51
|
+
shortExpr: `${s(b.shortRecord)} / ${budget}`,
|
|
52
|
+
})),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PERCENT = { unit: "percentunit", decimals: 3 };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A dashboard for one SLO: the SLI over its window against the objective,
|
|
60
|
+
* the error budget left, and each burn-rate alert's two windows with the
|
|
61
|
+
* rate it fires at drawn as a threshold line.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* import { Slo } from "@intentius/chant-lexicon-prometheus";
|
|
66
|
+
* import { SloDashboard } from "@intentius/chant-lexicon-grafana";
|
|
67
|
+
*
|
|
68
|
+
* export const checkout = Slo({ name: "checkout", objective: 0.999, window: "30d", sli: { ... } });
|
|
69
|
+
* export const checkoutSlo = SloDashboard({ slo: checkout, datasource: prometheus });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export const SloDashboard = Composite<SloDashboardProps, SloDashboardMembers>((props) => {
|
|
73
|
+
requireDatasource("SloDashboard", props.datasource);
|
|
74
|
+
if (!props.slo) throw new Error("SloDashboard: slo is required (an Slo(...) declaration)");
|
|
75
|
+
const m = isMetrics(props.slo) ? props.slo : sloMetrics(props.slo);
|
|
76
|
+
const q = sloQueries(m);
|
|
77
|
+
const ds = props.datasource;
|
|
78
|
+
const objective = num(m.objective * 100);
|
|
79
|
+
|
|
80
|
+
const sli = new StatPanel({
|
|
81
|
+
title: `SLI over ${m.window}`,
|
|
82
|
+
description: `Share of good events over the last ${m.window}. Green at or above the ${objective}% objective.`,
|
|
83
|
+
datasource: ds,
|
|
84
|
+
gridPos: { w: 6, h: 5 },
|
|
85
|
+
targets: [new PromQuery({ expr: q.sli, legendFormat: "SLI" })],
|
|
86
|
+
options: { colorMode: "background", graphMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
|
|
87
|
+
fieldConfig: { defaults: { ...PERCENT, thresholds: thresholds("red", [[m.objective, "green"]]) } },
|
|
88
|
+
});
|
|
89
|
+
const target = new StatPanel({
|
|
90
|
+
title: "Objective",
|
|
91
|
+
datasource: ds,
|
|
92
|
+
gridPos: { w: 6, h: 5 },
|
|
93
|
+
targets: [new PromQuery({ expr: q.objective, legendFormat: "objective" })],
|
|
94
|
+
options: { colorMode: "none", graphMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
|
|
95
|
+
fieldConfig: { defaults: PERCENT },
|
|
96
|
+
});
|
|
97
|
+
const remaining = new StatPanel({
|
|
98
|
+
title: "Error budget remaining",
|
|
99
|
+
description: `Share of the ${m.window} error budget (${num(m.errorBudget * 100)}% of events) not yet spent. Below 0 the objective is missed.`,
|
|
100
|
+
datasource: ds,
|
|
101
|
+
gridPos: { w: 6, h: 5 },
|
|
102
|
+
targets: [new PromQuery({ expr: q.errorBudgetRemaining, legendFormat: "remaining" })],
|
|
103
|
+
options: { colorMode: "background", graphMode: "area", reduceOptions: { calcs: ["lastNotNull"] } },
|
|
104
|
+
fieldConfig: { defaults: { unit: "percentunit", decimals: 1, thresholds: thresholds("red", [[0, "orange"], [0.25, "green"]]) } },
|
|
105
|
+
});
|
|
106
|
+
const firing = new StatPanel({
|
|
107
|
+
title: "Burn-rate alerts firing",
|
|
108
|
+
description: `${m.alertName} alerts for this SLO, any tier.`,
|
|
109
|
+
datasource: ds,
|
|
110
|
+
gridPos: { w: 6, h: 5 },
|
|
111
|
+
targets: [new PromQuery({ expr: q.alertsFiring, legendFormat: "firing" })],
|
|
112
|
+
options: { colorMode: "background", graphMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
|
|
113
|
+
fieldConfig: { defaults: { unit: "none", decimals: 0, thresholds: thresholds("green", [[1, "red"]]) } },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const sliOverTime = new TimeSeriesPanel({
|
|
117
|
+
title: `SLI over ${m.window}`,
|
|
118
|
+
description: `The dashed line is the ${objective}% objective.`,
|
|
119
|
+
datasource: ds,
|
|
120
|
+
gridPos: { w: 12, h: 8 },
|
|
121
|
+
targets: [new PromQuery({ expr: q.sli, legendFormat: "SLI" })],
|
|
122
|
+
fieldConfig: {
|
|
123
|
+
defaults: { ...PERCENT, thresholds: thresholds("red", [[m.objective, "green"]]), custom: { thresholdsStyle: { mode: "dashed" } } },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const budgetOverTime = new TimeSeriesPanel({
|
|
127
|
+
title: "Error budget remaining",
|
|
128
|
+
datasource: ds,
|
|
129
|
+
gridPos: { w: 12, h: 8 },
|
|
130
|
+
targets: [new PromQuery({ expr: q.errorBudgetRemaining, legendFormat: "remaining" })],
|
|
131
|
+
fieldConfig: {
|
|
132
|
+
defaults: { unit: "percentunit", decimals: 1, thresholds: thresholds("red", [[0, "green"]]), custom: { thresholdsStyle: { mode: "dashed" } } },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const rows = [new Row({ title: "Objective", panels: [sli, target, remaining, firing, sliOverTime, budgetOverTime] })];
|
|
137
|
+
|
|
138
|
+
if (q.burnRates.length > 0) {
|
|
139
|
+
const panels = q.burnRates.map(
|
|
140
|
+
(b) =>
|
|
141
|
+
new TimeSeriesPanel({
|
|
142
|
+
title: `Burn rate ${b.long} / ${b.short} (${b.severity})`,
|
|
143
|
+
description:
|
|
144
|
+
`Error ratio over the budget: 1 spends the ${m.window} budget exactly over ${m.window}. ` +
|
|
145
|
+
`${b.alert} fires with severity ${b.severity} while both windows are above ${num(b.factor)}, which spends the budget in ${b.exhaustsIn}.`,
|
|
146
|
+
datasource: ds,
|
|
147
|
+
gridPos: { w: 12, h: 8 },
|
|
148
|
+
targets: [
|
|
149
|
+
new PromQuery({ expr: b.longExpr, legendFormat: b.long }),
|
|
150
|
+
new PromQuery({ expr: b.shortExpr, legendFormat: b.short }),
|
|
151
|
+
],
|
|
152
|
+
fieldConfig: {
|
|
153
|
+
defaults: {
|
|
154
|
+
unit: "suffix:x",
|
|
155
|
+
decimals: 2,
|
|
156
|
+
min: 0,
|
|
157
|
+
thresholds: thresholds("green", [[b.factor, "red"]]),
|
|
158
|
+
custom: { thresholdsStyle: { mode: "dashed" } },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
rows.push(new Row({ title: "Burn rate by alert window", panels }));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const dashboard = new Dashboard({
|
|
167
|
+
...dashboardProps(props, {
|
|
168
|
+
title: `SLO: ${m.name}`,
|
|
169
|
+
uid: slugUid(`slo-${m.name}`),
|
|
170
|
+
description: `${objective}% over ${m.window}: SLI, error budget and burn rates for the ${m.name} SLO.`,
|
|
171
|
+
tags: ["slo", m.name],
|
|
172
|
+
}),
|
|
173
|
+
time: props.time ?? { from: "now-7d", to: "now" },
|
|
174
|
+
panels: rows,
|
|
175
|
+
});
|
|
176
|
+
return { dashboard };
|
|
177
|
+
}, "SloDashboard");
|
package/src/dashboard.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Dashboard` and `DashboardProvider`.
|
|
3
|
+
*
|
|
4
|
+
* A dashboard is written to `dashboards/<folder>/<uid>.json`, the JSON
|
|
5
|
+
* Grafana imports as it is. A provider is the entry in
|
|
6
|
+
* `provisioning/dashboards/chant.yaml` that points Grafana at those files;
|
|
7
|
+
* a build with dashboards and no declared provider gets a default one.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createResource } from "@intentius/chant/runtime";
|
|
11
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
12
|
+
import type { PanelEntity, RowEntity } from "./panels";
|
|
13
|
+
import type { VariableEntity } from "./variables";
|
|
14
|
+
import type { DashboardLink, TimePickerConfig } from "./schema/dashboard.gen";
|
|
15
|
+
|
|
16
|
+
/** A dashboard link: `title` and `type` required, the rest defaulted as Grafana's editor would. */
|
|
17
|
+
export type DashboardLinkInput = Partial<DashboardLink> & Pick<DashboardLink, "title" | "type">;
|
|
18
|
+
|
|
19
|
+
export interface DashboardProps {
|
|
20
|
+
title: string;
|
|
21
|
+
/**
|
|
22
|
+
* Stable id: links, provisioning and the API address a dashboard by it.
|
|
23
|
+
* Letters, digits, `-` and `_`, at most 40 characters. Defaults to the
|
|
24
|
+
* export name as a uid (`checkoutService` becomes `checkout-service`).
|
|
25
|
+
*/
|
|
26
|
+
uid?: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
tags?: string[];
|
|
29
|
+
/** Initial time range. Defaults to the last 6 hours. */
|
|
30
|
+
time?: { from: string; to: string };
|
|
31
|
+
/** Auto-refresh interval, e.g. `30s`. */
|
|
32
|
+
refresh?: string;
|
|
33
|
+
/** `browser`, `utc` or an IANA zone. Defaults to `browser`. */
|
|
34
|
+
timezone?: string;
|
|
35
|
+
weekStart?: string;
|
|
36
|
+
fiscalYearStartMonth?: number;
|
|
37
|
+
liveNow?: boolean;
|
|
38
|
+
timepicker?: TimePickerConfig;
|
|
39
|
+
/** Shared crosshair or tooltip across panels. */
|
|
40
|
+
graphTooltip?: "default" | "sharedCrosshair" | "sharedTooltip";
|
|
41
|
+
/** Whether users may edit it in the UI. Defaults to true, as in Grafana. */
|
|
42
|
+
editable?: boolean;
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
variables?: VariableEntity<any>[];
|
|
45
|
+
/** Panels and rows, top to bottom. A row's own `panels` follow it. */
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
panels?: Array<PanelEntity<any, any> | RowEntity>;
|
|
48
|
+
links?: DashboardLinkInput[];
|
|
49
|
+
/**
|
|
50
|
+
* The Grafana folder to provision it into. Written as a subdirectory of
|
|
51
|
+
* `dashboards/`, which the provider maps to folders
|
|
52
|
+
* (`foldersFromFilesStructure`). Leave it out for the General folder.
|
|
53
|
+
*/
|
|
54
|
+
folder?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DashboardEntity extends Declarable {
|
|
58
|
+
readonly props: DashboardProps;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const DASHBOARD_TYPE = "Grafana::Dashboard";
|
|
62
|
+
|
|
63
|
+
const DashboardBase = createResource(DASHBOARD_TYPE, "grafana", {}) as unknown as (this: object, props: Record<string, unknown>) => void;
|
|
64
|
+
|
|
65
|
+
/** A Grafana dashboard. */
|
|
66
|
+
export const Dashboard = function (this: object, props: DashboardProps) {
|
|
67
|
+
DashboardBase.call(this, props as unknown as Record<string, unknown>);
|
|
68
|
+
} as unknown as new (props: DashboardProps) => DashboardEntity;
|
|
69
|
+
Object.defineProperty(Dashboard, "name", { value: "Dashboard" });
|
|
70
|
+
|
|
71
|
+
export function isDashboardEntity(value: unknown): value is DashboardEntity {
|
|
72
|
+
return (
|
|
73
|
+
typeof value === "object" &&
|
|
74
|
+
value !== null &&
|
|
75
|
+
(value as Declarable).entityType === DASHBOARD_TYPE &&
|
|
76
|
+
(value as Declarable).lexicon === "grafana"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Where Grafana finds the dashboard files by default: mount `dashboards/` here. */
|
|
81
|
+
export const DEFAULT_DASHBOARDS_PATH = "/var/lib/grafana/dashboards";
|
|
82
|
+
|
|
83
|
+
export interface DashboardProviderProps {
|
|
84
|
+
/** Provider name, unique in the provisioning file. */
|
|
85
|
+
name: string;
|
|
86
|
+
orgId?: number;
|
|
87
|
+
/** Folder for dashboards not in a subdirectory. */
|
|
88
|
+
folder?: string;
|
|
89
|
+
folderUid?: string;
|
|
90
|
+
/** Where the `dashboards/` output is mounted in the Grafana container. Defaults to `/var/lib/grafana/dashboards`. */
|
|
91
|
+
path?: string;
|
|
92
|
+
/** Map subdirectories to Grafana folders. Defaults to true, which is what a dashboard's `folder` relies on. */
|
|
93
|
+
foldersFromFilesStructure?: boolean;
|
|
94
|
+
disableDeletion?: boolean;
|
|
95
|
+
allowUiUpdates?: boolean;
|
|
96
|
+
updateIntervalSeconds?: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface DashboardProviderEntity extends Declarable {
|
|
100
|
+
readonly props: DashboardProviderProps;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const DASHBOARD_PROVIDER_TYPE = "Grafana::DashboardProvider";
|
|
104
|
+
|
|
105
|
+
const ProviderBase = createResource(DASHBOARD_PROVIDER_TYPE, "grafana", {}) as unknown as (
|
|
106
|
+
this: object,
|
|
107
|
+
props: Record<string, unknown>,
|
|
108
|
+
) => void;
|
|
109
|
+
|
|
110
|
+
/** An entry in the dashboard provisioning file. Optional: a default one is written when none is declared. */
|
|
111
|
+
export const DashboardProvider = function (this: object, props: DashboardProviderProps) {
|
|
112
|
+
ProviderBase.call(this, props as unknown as Record<string, unknown>);
|
|
113
|
+
} as unknown as new (props: DashboardProviderProps) => DashboardProviderEntity;
|
|
114
|
+
Object.defineProperty(DashboardProvider, "name", { value: "DashboardProvider" });
|
|
115
|
+
|
|
116
|
+
export function isDashboardProviderEntity(value: unknown): value is DashboardProviderEntity {
|
|
117
|
+
return (
|
|
118
|
+
typeof value === "object" &&
|
|
119
|
+
value !== null &&
|
|
120
|
+
(value as Declarable).entityType === DASHBOARD_PROVIDER_TYPE &&
|
|
121
|
+
(value as Declarable).lexicon === "grafana"
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Datasource`: a Grafana datasource, declared once.
|
|
3
|
+
*
|
|
4
|
+
* A declared datasource is written to the datasource provisioning file, and
|
|
5
|
+
* panels, queries and variables hold the entity itself rather than a uid
|
|
6
|
+
* string. The class is generic in the plugin type, so a `PromQuery` cannot
|
|
7
|
+
* be pointed at a Tempo datasource without a type error.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createResource } from "@intentius/chant/runtime";
|
|
11
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
12
|
+
import { slugUid } from "./util";
|
|
13
|
+
|
|
14
|
+
/** Datasource plugin types this lexicon types queries for. Any other plugin id is accepted as a string. */
|
|
15
|
+
export type KnownDatasourceType = "prometheus" | "tempo" | "loki";
|
|
16
|
+
|
|
17
|
+
/** How Grafana reaches the datasource: through its backend (`proxy`) or from the browser (`direct`). */
|
|
18
|
+
export type DatasourceAccess = "proxy" | "direct";
|
|
19
|
+
|
|
20
|
+
export interface DatasourceProps<T extends string = string> {
|
|
21
|
+
/** Display name, unique within the Grafana organisation. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** The datasource plugin id, e.g. `prometheus`, `tempo`, `loki`. */
|
|
24
|
+
type: T;
|
|
25
|
+
/** Stable id panels refer to. Defaults to the name as a uid (`Prometheus` becomes `prometheus`). */
|
|
26
|
+
uid?: string;
|
|
27
|
+
url?: string;
|
|
28
|
+
access?: DatasourceAccess;
|
|
29
|
+
isDefault?: boolean;
|
|
30
|
+
basicAuth?: boolean;
|
|
31
|
+
basicAuthUser?: string;
|
|
32
|
+
user?: string;
|
|
33
|
+
database?: string;
|
|
34
|
+
withCredentials?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Plugin settings. A declared `Datasource` anywhere in here is written as
|
|
37
|
+
* its uid, so `tracesToLogsV2: { datasourceUid: loki }` links Tempo to a
|
|
38
|
+
* declared Loki.
|
|
39
|
+
*/
|
|
40
|
+
jsonData?: Record<string, unknown>;
|
|
41
|
+
/**
|
|
42
|
+
* Secrets. Write them as Grafana provisioning expands them
|
|
43
|
+
* (`$__env{NAME}`, `$__file{/path}`, `${NAME}`), not as literals: GRAF002
|
|
44
|
+
* flags a literal.
|
|
45
|
+
*/
|
|
46
|
+
secureJsonData?: Record<string, string>;
|
|
47
|
+
/** Whether users may edit the provisioned datasource in the UI. Defaults to false. */
|
|
48
|
+
editable?: boolean;
|
|
49
|
+
orgId?: number;
|
|
50
|
+
version?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface DatasourceEntity<T extends string = string> extends Declarable {
|
|
54
|
+
readonly props: DatasourceProps<T>;
|
|
55
|
+
/** The plugin id. */
|
|
56
|
+
readonly datasourceType: T;
|
|
57
|
+
/** The uid panels will reference. */
|
|
58
|
+
readonly uid: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const DATASOURCE_TYPE = "Grafana::Datasource";
|
|
62
|
+
|
|
63
|
+
const Base = createResource(DATASOURCE_TYPE, "grafana", {}) as unknown as (this: object, props: Record<string, unknown>) => void;
|
|
64
|
+
|
|
65
|
+
export interface DatasourceConstructor {
|
|
66
|
+
new <T extends string>(props: DatasourceProps<T>): DatasourceEntity<T>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A Grafana datasource, provisioned from `provisioning/datasources/chant.yaml`. */
|
|
70
|
+
export const Datasource = function (this: object, props: DatasourceProps) {
|
|
71
|
+
Base.call(this, props as unknown as Record<string, unknown>);
|
|
72
|
+
Object.defineProperty(this, "datasourceType", { value: props.type, enumerable: false });
|
|
73
|
+
Object.defineProperty(this, "uid", { value: props.uid ?? slugUid(props.name), enumerable: false });
|
|
74
|
+
} as unknown as DatasourceConstructor;
|
|
75
|
+
Object.defineProperty(Datasource, "name", { value: "Datasource" });
|
|
76
|
+
|
|
77
|
+
export function isDatasourceEntity(value: unknown): value is DatasourceEntity {
|
|
78
|
+
return (
|
|
79
|
+
typeof value === "object" &&
|
|
80
|
+
value !== null &&
|
|
81
|
+
(value as Declarable).entityType === DATASOURCE_TYPE &&
|
|
82
|
+
(value as Declarable).lexicon === "grafana"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** A datasource declared somewhere this build can't see, named by its Grafana ref. */
|
|
87
|
+
export interface DatasourceRef<T extends string = string> {
|
|
88
|
+
type: T;
|
|
89
|
+
uid: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Grafana's own pseudo-datasources, which need no declaration. */
|
|
93
|
+
export const BUILTIN_DATASOURCE_UIDS: ReadonlySet<string> = new Set(["grafana", "-- Grafana --", "-- Mixed --", "-- Dashboard --"]);
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a parsed Grafana document looks like. Kept free of the plugin and the
|
|
3
|
+
* TypeScript compiler so it bundles for edge runtimes, like the other
|
|
4
|
+
* lexicons' `detect` modules.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function isObject(v: unknown): v is Record<string, unknown> {
|
|
8
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Dashboard JSON: a `panels` array plus a `schemaVersion` or a `templating` block. */
|
|
12
|
+
export function looksLikeDashboard(data: unknown): data is Record<string, unknown> {
|
|
13
|
+
if (!isObject(data)) return false;
|
|
14
|
+
return Array.isArray(data.panels) && (typeof data.schemaVersion === "number" || isObject(data.templating));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A datasource provisioning file: `apiVersion` and a `datasources` list. */
|
|
18
|
+
export function looksLikeDatasourceProvisioning(data: unknown): data is { apiVersion: unknown; datasources: unknown[] } {
|
|
19
|
+
return isObject(data) && data.apiVersion !== undefined && Array.isArray(data.datasources);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A dashboard provisioning file: `apiVersion` and a `providers` list. */
|
|
23
|
+
export function looksLikeDashboardProvisioning(data: unknown): data is { apiVersion: unknown; providers: unknown[] } {
|
|
24
|
+
return isObject(data) && data.apiVersion !== undefined && Array.isArray(data.providers);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Template detection for `chant import` and friends: any of the three. */
|
|
28
|
+
export function detectTemplate(data: unknown): boolean {
|
|
29
|
+
return looksLikeDashboard(data) || looksLikeDatasourceProvisioning(data) || looksLikeDashboardProvisioning(data);
|
|
30
|
+
}
|