@intentius/chant-lexicon-grafana 0.124.30 → 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.
Files changed (166) hide show
  1. package/dist/actions/grafana.js +36 -0
  2. package/dist/actions/index.js +1 -0
  3. package/dist/alerting-build.js +262 -0
  4. package/dist/alerting.js +125 -0
  5. package/dist/annotations.js +47 -0
  6. package/dist/api/apply.js +320 -0
  7. package/dist/api/bind.js +44 -0
  8. package/dist/api/client.js +113 -0
  9. package/dist/api/converge.js +61 -0
  10. package/dist/api/dashboards.js +210 -0
  11. package/dist/api/datasources.js +24 -0
  12. package/dist/api/fake-grafana-writes.js +185 -0
  13. package/dist/api/fake-grafana.js +161 -0
  14. package/dist/api/folders.js +290 -0
  15. package/dist/api/library-panels.js +82 -0
  16. package/dist/build.js +767 -0
  17. package/dist/catalog.js +198 -0
  18. package/dist/codegen/docs-cli.js +3 -0
  19. package/dist/codegen/docs.js +130 -0
  20. package/dist/codegen/generate-cli.js +7 -0
  21. package/dist/codegen/generate.js +97 -0
  22. package/dist/codegen/package.js +28 -0
  23. package/dist/codegen/schema-types.js +159 -0
  24. package/dist/composites/agent-dashboard.js +460 -0
  25. package/dist/composites/catalog.js +306 -0
  26. package/dist/composites/index.js +8 -0
  27. package/dist/composites/red-dashboard.js +135 -0
  28. package/dist/composites/shared.js +74 -0
  29. package/dist/composites/slo-alert-rules.js +96 -0
  30. package/dist/composites/slo-dashboard.js +153 -0
  31. package/dist/config.js +110 -0
  32. package/dist/contact-point-check.js +61 -0
  33. package/dist/contact-point-secrets.js +32 -0
  34. package/dist/contact-point-settings.gen.js +421 -0
  35. package/dist/dashboard.js +60 -0
  36. package/dist/datasource-refs.js +170 -0
  37. package/dist/datasource-settings.js +57 -0
  38. package/dist/datasource.js +68 -0
  39. package/dist/deep-observe-hooks.js +266 -0
  40. package/dist/deep-observe.js +171 -0
  41. package/dist/describe-resources.js +249 -0
  42. package/dist/detect.js +119 -0
  43. package/dist/export-resources.js +73 -0
  44. package/dist/folder.js +86 -0
  45. package/dist/import/alerting.js +514 -0
  46. package/dist/import/edits.js +90 -0
  47. package/dist/import/embedded.js +60 -0
  48. package/dist/import/generator.js +656 -0
  49. package/dist/import/live-export.js +246 -0
  50. package/dist/import/mappings.js +317 -0
  51. package/dist/import/model.js +23 -0
  52. package/dist/import/normalize-alerting.js +106 -0
  53. package/dist/import/normalize.js +426 -0
  54. package/dist/import/operator.js +252 -0
  55. package/dist/import/parser.js +1480 -0
  56. package/dist/import/report.js +58 -0
  57. package/dist/import/testdata/fixtures.js +122 -0
  58. package/dist/import/v2.js +814 -0
  59. package/dist/index.js +59 -0
  60. package/dist/init-templates.js +305 -0
  61. package/dist/integrity.json +2 -2
  62. package/dist/k8s.js +548 -0
  63. package/dist/library-panel.js +83 -0
  64. package/dist/lint/audit-catalog.js +40 -0
  65. package/dist/lint/audit-lineage.js +9 -0
  66. package/dist/lint/post-synth/graf101.js +13 -0
  67. package/dist/lint/post-synth/graf102.js +13 -0
  68. package/dist/lint/post-synth/graf103.js +13 -0
  69. package/dist/lint/post-synth/graf104.js +13 -0
  70. package/dist/lint/post-synth/graf105.js +13 -0
  71. package/dist/lint/post-synth/graf106.js +13 -0
  72. package/dist/lint/post-synth/graf107.js +13 -0
  73. package/dist/lint/post-synth/graf108.js +13 -0
  74. package/dist/lint/post-synth/graf109.js +13 -0
  75. package/dist/lint/post-synth/graf110.js +13 -0
  76. package/dist/lint/post-synth/graf111.js +13 -0
  77. package/dist/lint/post-synth/graf112.js +13 -0
  78. package/dist/lint/post-synth/graf113.js +13 -0
  79. package/dist/lint/post-synth/graf114.js +13 -0
  80. package/dist/lint/post-synth/graf115.js +13 -0
  81. package/dist/lint/post-synth/graf116.js +13 -0
  82. package/dist/lint/post-synth/graf117.js +13 -0
  83. package/dist/lint/post-synth/graf118.js +37 -0
  84. package/dist/lint/post-synth/grafana-helpers.js +89 -0
  85. package/dist/lint/post-synth/index.js +38 -0
  86. package/dist/lint/rules/grafana-ast.js +70 -0
  87. package/dist/lint/rules/index.js +6 -0
  88. package/dist/lint/rules/literal-secret.js +109 -0
  89. package/dist/lint/rules/uid-syntax.js +53 -0
  90. package/dist/lsp/completions.js +11 -0
  91. package/dist/lsp/hover.js +29 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/op/activities/grafana-apply.js +147 -0
  94. package/dist/op/activities/index.js +7 -0
  95. package/dist/op/builders.js +19 -0
  96. package/dist/ownership.js +102 -0
  97. package/dist/package-cli.js +14 -0
  98. package/dist/panel-options.js +12 -0
  99. package/dist/panels.js +296 -0
  100. package/dist/pin.js +113 -0
  101. package/dist/plugin.js +172 -0
  102. package/dist/promql-check.js +176 -0
  103. package/dist/query-models.js +13 -0
  104. package/dist/query-syntax.js +129 -0
  105. package/dist/query.js +227 -0
  106. package/dist/schema/annotationslist.gen.js +5 -0
  107. package/dist/schema/azuremonitor.gen.js +5 -0
  108. package/dist/schema/barchart.gen.js +5 -0
  109. package/dist/schema/bargauge.gen.js +5 -0
  110. package/dist/schema/bigquery.gen.js +5 -0
  111. package/dist/schema/candlestick.gen.js +5 -0
  112. package/dist/schema/canvas.gen.js +5 -0
  113. package/dist/schema/cloudwatch.gen.js +5 -0
  114. package/dist/schema/dashboard.gen.js +6 -0
  115. package/dist/schema/dashboardlist.gen.js +5 -0
  116. package/dist/schema/datagrid.gen.js +5 -0
  117. package/dist/schema/elasticsearch.gen.js +5 -0
  118. package/dist/schema/expr.gen.js +5 -0
  119. package/dist/schema/gauge.gen.js +5 -0
  120. package/dist/schema/geomap.gen.js +5 -0
  121. package/dist/schema/googlecloudmonitoring.gen.js +5 -0
  122. package/dist/schema/grafanapyroscope.gen.js +5 -0
  123. package/dist/schema/heatmap.gen.js +5 -0
  124. package/dist/schema/histogram.gen.js +5 -0
  125. package/dist/schema/index.js +40 -0
  126. package/dist/schema/logs.gen.js +5 -0
  127. package/dist/schema/loki.gen.js +5 -0
  128. package/dist/schema/news.gen.js +5 -0
  129. package/dist/schema/nodegraph.gen.js +5 -0
  130. package/dist/schema/piechart.gen.js +5 -0
  131. package/dist/schema/prometheus.gen.js +5 -0
  132. package/dist/schema/stat.gen.js +5 -0
  133. package/dist/schema/statetimeline.gen.js +5 -0
  134. package/dist/schema/statushistory.gen.js +5 -0
  135. package/dist/schema/table.gen.js +5 -0
  136. package/dist/schema/tempo.gen.js +5 -0
  137. package/dist/schema/text.gen.js +5 -0
  138. package/dist/schema/timeseries.gen.js +5 -0
  139. package/dist/schema/trend.gen.js +5 -0
  140. package/dist/schema/xychart.gen.js +5 -0
  141. package/dist/schema-validate.js +265 -0
  142. package/dist/serializer.js +40 -0
  143. package/dist/skill-defs.js +60 -0
  144. package/dist/spec/fetch-cli.js +26 -0
  145. package/dist/spec/fetch-notifiers-cli.js +53 -0
  146. package/dist/spec/fetch-units-cli.js +40 -0
  147. package/dist/spec/notifiers.js +126 -0
  148. package/dist/spec/overlay.js +92 -0
  149. package/dist/spec/schemas.gen.js +42 -0
  150. package/dist/spec/schemas.js +62 -0
  151. package/dist/spec/units.gen.js +280 -0
  152. package/dist/spec/units.js +72 -0
  153. package/dist/transformations.js +127 -0
  154. package/dist/util.js +34 -0
  155. package/dist/validate-alerting.js +444 -0
  156. package/dist/validate-cli.js +7 -0
  157. package/dist/validate-output.js +635 -0
  158. package/dist/validate.js +122 -0
  159. package/dist/validation.js +13 -0
  160. package/dist/variables.js +70 -0
  161. package/package.json +10 -10
  162. package/src/codegen/package.ts +2 -2
  163. package/src/spec/fetch-notifiers-cli.ts +2 -2
  164. package/src/spec/fetch-units-cli.ts +2 -2
  165. package/src/spec/overlay.ts +2 -2
  166. package/src/spec/schemas.ts +2 -2
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The built-in entity catalog: every class this package exports, keyed by
3
+ * class name. It feeds the packaged registry (`dist/meta.json`), LSP
4
+ * completions and hover, and the docs.
5
+ */
6
+ import { DATASOURCE_PROVISIONING_TYPE, DATASOURCE_TYPE, EXTERNAL_DATASOURCE_TYPE } from "./datasource.js";
7
+ import { FOLDER_TYPE } from "./folder.js";
8
+ import { LIBRARY_PANEL_REF_TYPE, LIBRARY_PANEL_TYPE } from "./library-panel.js";
9
+ import { DASHBOARD_TYPE, DASHBOARD_PROVIDER_TYPE } from "./dashboard.js";
10
+ import * as panels from "./panels.js";
11
+ import * as queries from "./query.js";
12
+ import { VARIABLE_TYPE_PREFIX } from "./variables.js";
13
+ import { ALERT_QUERY_TYPE, ALERT_RULE_GROUP_TYPE, ALERT_RULE_TYPE, CONTACT_POINT_TYPE, EXPRESSION_TYPE_PREFIX, MUTE_TIMING_TYPE, NOTIFICATION_POLICY_TYPE, NOTIFICATION_TEMPLATE_TYPE, } from "./alerting.js";
14
+ function isClassWith(v, key) {
15
+ return typeof v === "function" && typeof v[key] === "object";
16
+ }
17
+ const panelEntries = Object.entries(panels)
18
+ .filter((e) => isClassWith(e[1], "definition"))
19
+ .map(([className, cls]) => ({
20
+ className,
21
+ entityType: `${panels.PANEL_TYPE_PREFIX}${cls.definition.type}`,
22
+ kind: "panel",
23
+ entityKind: "property",
24
+ pluginId: cls.definition.type,
25
+ description: cls.definition.description ?? "",
26
+ }));
27
+ const queryEntries = Object.entries(queries)
28
+ .filter((e) => isClassWith(e[1], "definition"))
29
+ .map(([className, cls]) => ({
30
+ className,
31
+ entityType: `${queries.QUERY_TYPE_PREFIX}${cls.definition.datasourceType}`,
32
+ kind: "query",
33
+ entityKind: "property",
34
+ pluginId: cls.definition.datasourceType,
35
+ description: cls.definition.description ?? "",
36
+ }));
37
+ const VARIABLES = [
38
+ ["QueryVariable", "query", "A variable whose values come from a datasource query"],
39
+ ["CustomVariable", "custom", "A variable with a fixed list of values"],
40
+ ["IntervalVariable", "interval", "A time interval to choose, for range selectors"],
41
+ ["DatasourceVariable", "datasource", "A choice of datasource of one plugin type; usable wherever a datasource is"],
42
+ ["ConstantVariable", "constant", "A hidden, fixed value"],
43
+ ["TextboxVariable", "textbox", "A free-text box"],
44
+ ["AdhocVariable", "adhoc", "Key/value filters Grafana adds to every query sent to the variable's datasource"],
45
+ ["GroupByVariable", "groupby", "A choice of label keys Grafana groups every query to the variable's datasource by (experimental in Grafana)"],
46
+ ["SwitchVariable", "switch", "An on/off switch with a value for each state (Grafana 12.3 and later)"],
47
+ ];
48
+ const EXPRESSIONS = [
49
+ ["ReduceExpression", "reduce", "A server-side expression reducing each series to one number (last, mean, max, ...)"],
50
+ ["MathExpression", "math", "A server-side math expression over other results by refId, e.g. $A / $B"],
51
+ ["ThresholdExpression", "threshold", "A server-side threshold, with an optional recovery threshold"],
52
+ ["ResampleExpression", "resample", "A server-side expression resampling a time series to a fixed window"],
53
+ ["ClassicConditionsExpression", "classic_conditions", "Grafana's legacy classic conditions, as a server-side expression"],
54
+ ["SqlExpression", "sql", "A server-side SQL expression over the other results"],
55
+ ];
56
+ export const BUILTIN_CATALOG = [
57
+ {
58
+ className: "Dashboard",
59
+ entityType: DASHBOARD_TYPE,
60
+ kind: "dashboard",
61
+ entityKind: "resource",
62
+ description: "A dashboard: title, uid, time range, variables, rows, panels and links, written as the JSON Grafana imports",
63
+ },
64
+ {
65
+ className: "Datasource",
66
+ entityType: DATASOURCE_TYPE,
67
+ kind: "datasource",
68
+ entityKind: "resource",
69
+ description: "A datasource, declared once and referenced by panels, queries and variables",
70
+ },
71
+ {
72
+ className: "ExternalDatasource",
73
+ entityType: EXTERNAL_DATASOURCE_TYPE,
74
+ kind: "datasource",
75
+ entityKind: "resource",
76
+ description: "A datasource that already exists in Grafana: referenced like a Datasource and checked by GRAF101/GRAF102, never provisioned",
77
+ },
78
+ {
79
+ className: "Folder",
80
+ entityType: FOLDER_TYPE,
81
+ kind: "folder",
82
+ entityKind: "resource",
83
+ description: "A folder with a stable uid, nested with parent; a dashboard's folder may be one instead of a path",
84
+ },
85
+ {
86
+ className: "LibraryPanel",
87
+ entityType: LIBRARY_PANEL_TYPE,
88
+ kind: "library",
89
+ entityKind: "resource",
90
+ description: "A panel kept in Grafana's library and shared by dashboards; the build writes it into their __elements, and the API applier into the library",
91
+ },
92
+ {
93
+ className: "DatasourceProvisioning",
94
+ entityType: DATASOURCE_PROVISIONING_TYPE,
95
+ kind: "provider",
96
+ entityKind: "resource",
97
+ description: "The datasource provisioning file's prune (on by default) and deleteDatasources",
98
+ },
99
+ {
100
+ className: "DashboardProvider",
101
+ entityType: DASHBOARD_PROVIDER_TYPE,
102
+ kind: "provider",
103
+ entityKind: "resource",
104
+ description: "An entry in the dashboard provisioning file; a default one is written when none is declared",
105
+ },
106
+ {
107
+ className: "Row",
108
+ entityType: panels.ROW_TYPE,
109
+ kind: "row",
110
+ entityKind: "property",
111
+ description: "A full-width row header; its panels are placed below it, or inside it when collapsed",
112
+ },
113
+ {
114
+ className: "LibraryPanelRef",
115
+ entityType: LIBRARY_PANEL_REF_TYPE,
116
+ kind: "library",
117
+ entityKind: "property",
118
+ description: "A library panel placed on a dashboard, with its own gridPos, id and title; names a LibraryPanel, or { uid, name } of one already in Grafana",
119
+ },
120
+ {
121
+ className: "AlertRuleGroup",
122
+ entityType: ALERT_RULE_GROUP_TYPE,
123
+ kind: "alerting",
124
+ entityKind: "resource",
125
+ description: "A group of Grafana-managed alert and recording rules in a folder, evaluated together at one interval",
126
+ },
127
+ {
128
+ className: "ContactPoint",
129
+ entityType: CONTACT_POINT_TYPE,
130
+ kind: "alerting",
131
+ entityKind: "resource",
132
+ description: "A contact point: the integrations (email, Slack, webhook, ...) alerts are sent to, under one name",
133
+ },
134
+ {
135
+ className: "NotificationPolicy",
136
+ entityType: NOTIFICATION_POLICY_TYPE,
137
+ kind: "alerting",
138
+ entityKind: "resource",
139
+ description: "An organisation's notification policy tree: the root receiver and the routes that match alerts to contact points",
140
+ },
141
+ {
142
+ className: "MuteTiming",
143
+ entityType: MUTE_TIMING_TYPE,
144
+ kind: "alerting",
145
+ entityKind: "resource",
146
+ description: "A named time interval that mutes (or, as active_time_intervals, enables) notifications",
147
+ },
148
+ {
149
+ className: "NotificationTemplate",
150
+ entityType: NOTIFICATION_TEMPLATE_TYPE,
151
+ kind: "alerting",
152
+ entityKind: "resource",
153
+ description: "A notification template group, Go templates contact point settings can use",
154
+ },
155
+ {
156
+ className: "AlertRule",
157
+ entityType: ALERT_RULE_TYPE,
158
+ kind: "alerting",
159
+ entityKind: "property",
160
+ description: "A Grafana-managed alert or recording rule: queries, expressions and the condition that fires it",
161
+ },
162
+ {
163
+ className: "AlertQuery",
164
+ entityType: ALERT_QUERY_TYPE,
165
+ kind: "alerting",
166
+ entityKind: "property",
167
+ description: "A datasource query of an alert rule, with its model as Grafana stores it and its time range",
168
+ },
169
+ ...EXPRESSIONS.map(([className, type, description]) => ({
170
+ className,
171
+ entityType: `${EXPRESSION_TYPE_PREFIX}${type}`,
172
+ kind: "expression",
173
+ entityKind: "property",
174
+ description,
175
+ })),
176
+ ...panelEntries,
177
+ ...queryEntries,
178
+ ...VARIABLES.map(([className, type, description]) => ({
179
+ className,
180
+ entityType: `${VARIABLE_TYPE_PREFIX}${type}`,
181
+ kind: "variable",
182
+ entityKind: "property",
183
+ description,
184
+ })),
185
+ ];
186
+ /**
187
+ * The catalog as a chant lexicon registry, keyed by class name. Every class
188
+ * is listed as a resource, including the property-kind panels, queries and
189
+ * variables: the registry drives `new …` completions, and an author writes
190
+ * `new TimeSeriesPanel(…)` the same way as `new Dashboard(…)`.
191
+ */
192
+ export function lexiconRegistry() {
193
+ const out = {};
194
+ for (const e of [...BUILTIN_CATALOG].sort((a, b) => a.className.localeCompare(b.className))) {
195
+ out[e.className] = { resourceType: e.entityType, kind: "resource", lexicon: "grafana" };
196
+ }
197
+ return out;
198
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ import { generateDocs } from "./docs.js";
3
+ await generateDocs({ verbose: true });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Documentation generation for the grafana lexicon: the generated reference
3
+ * pages from the core docs pipeline, plus the authored pages in docs/pages/.
4
+ */
5
+ import { dirname, join } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
8
+ function serviceFromType(resourceType) {
9
+ const parts = resourceType.split("::");
10
+ return parts.length >= 2 ? parts[1] : "Grafana";
11
+ }
12
+ const overview = `The grafana lexicon types what a [Grafana](https://grafana.com/docs/grafana/latest/) dashboard
13
+ shows: datasources, dashboards, rows, panels, queries and variables, and
14
+ Grafana-managed alerting. \`chant build\`
15
+ writes one JSON file per dashboard, the JSON Grafana imports as it is, plus the
16
+ provisioning files Grafana reads for dashboards and datasources. Grafana server
17
+ settings, users and plugins are out of scope.
18
+
19
+ \`\`\`ts
20
+ import { Datasource, PromQuery, TimeSeriesPanel, Dashboard } from "@intentius/chant-lexicon-grafana";
21
+
22
+ const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
23
+ const rate = new PromQuery({ expr: "sum(rate(http_requests_total[$__rate_interval]))" });
24
+ const requests = new TimeSeriesPanel({ title: "Requests per second", datasource: prometheus, targets: [rate] });
25
+
26
+ export const overview = new Dashboard({ title: "Overview", panels: [requests] });
27
+ \`\`\`
28
+
29
+ Panels: time series, stat, gauge, table, logs, traces, heatmap, text, bar
30
+ chart, bar gauge, pie chart, state timeline, status history, histogram, node
31
+ graph, XY chart, trend, canvas, geomap, candlestick, annotations list, dashboard list, news, data grid, flame graph and alert list, plus rows.
32
+ Queries: Prometheus (PromQL), Tempo (TraceQL), Loki (LogQL), Elasticsearch,
33
+ CloudWatch, Azure Monitor, Cloud Monitoring, BigQuery, Pyroscope, and SQL for
34
+ PostgreSQL, MySQL and MSSQL. Panel options and query fields are generated from
35
+ Grafana's own schemas at a pinned version, corrected against Grafana's CUE, and
36
+ track Grafana 12.4 and 13.x. Every build validates the dashboards against the
37
+ same schemas.
38
+ Dashboards are written in the classic (v1) JSON model. Grafana 13's v2 model
39
+ (tabs, auto grids, conditional rendering) cannot be declared yet; import and
40
+ drift read a v2 dashboard by converting it to the classic model, and say what
41
+ the conversion loses.
42
+ \`definePanel\` and \`defineQuery\` add plugins chant doesn't ship.
43
+ \`chant import\` turns dashboard JSON exported from Grafana into this TypeScript,
44
+ and it builds back to the same dashboard (see
45
+ [Importing Dashboards](./importing/)). \`chant lifecycle diff --live\` reports
46
+ a dashboard edited in Grafana as drift, property by property, and
47
+ \`chant import --from <env>\` writes a running Grafana's dashboards as
48
+ TypeScript (see [Drift and Live Export](./observing/)). \`grafanaApply\` writes
49
+ a build to a running Grafana over its HTTP API, with folders and library
50
+ panels, and prunes the project's own dashboards and folders (see
51
+ [Apply over the API](./applying/)).
52
+
53
+ Grafana-managed alerting is typed too: rule groups with their queries and
54
+ server-side expressions (reduce, math, threshold, resample, classic
55
+ conditions, SQL), contact points, the notification policy tree, mute timings
56
+ and templates, written to Grafana's alerting provisioning file.
57
+ \`SloAlertRules\` turns a prometheus \`Slo\` into burn-rate alert rules. See
58
+ [Alerting](./alerting/).
59
+
60
+ Three composites build whole dashboards from declarations in other lexicons:
61
+ \`RedDashboard\` from an otel \`spanmetrics\` connector, \`SloDashboard\` from a
62
+ prometheus \`Slo\` and \`AgentDashboard\` from the otel GenAI preset. Metric names
63
+ come from the declaration, so renaming one moves the panels. See
64
+ [Dashboards from Declarations](./composites/).
65
+
66
+ Checks catch a query aimed at a datasource nobody declared, with
67
+ \`Datasource\` or \`ExternalDatasource\` (GRAF101), or of the
68
+ wrong type (GRAF102), a \`$variable\` the dashboard doesn't declare (GRAF103),
69
+ duplicate uids and ids (GRAF104), panels off the grid or overlapping (GRAF105),
70
+ anything Grafana's dashboard schema rejects (GRAF107), PromQL sent to a
71
+ Prometheus that doesn't parse (GRAF108), LogQL sent to a Loki and TraceQL
72
+ sent to a Tempo that don't parse (GRAF116, GRAF117), dashboard providers that load a
73
+ dashboard twice or not into its declared folder (GRAF109), a panel repeated over a variable that only ever
74
+ holds one value (GRAF110), and a unit Grafana doesn't know (GRAF115). For alerting, they catch rules whose condition or expressions name
75
+ no query (GRAF111), queries to undeclared datasources (GRAF112), routes to
76
+ undeclared contact points or mute timings (GRAF113), and uids, intervals and
77
+ duplicates Grafana refuses (GRAF114).
78
+ `;
79
+ const outputFormat = `The grafana lexicon writes Grafana's own files, keyed by path under the
80
+ directory of the \`-o\` output:
81
+
82
+ - \`dashboards/<uid>.json\`, or \`dashboards/<folder>/<uid>.json\` for a
83
+ dashboard with a \`folder\`: the dashboard JSON, one file per dashboard.
84
+ - \`provisioning/datasources/chant.yaml\`: every declared datasource.
85
+ - \`provisioning/dashboards/chant.yaml\`: a file provider for the dashboards
86
+ directory; a default one unless a \`DashboardProvider\` is declared.
87
+ - \`provisioning/alerting/chant.yaml\`: rule groups, contact points, the
88
+ notification policy tree, mute timings and templates, when the build
89
+ declares any.
90
+
91
+ The primary output (the \`-o\` file itself) is a JSON index of the dashboards,
92
+ datasources, alerting and files that were built.
93
+
94
+ - Dashboard JSON carries the pinned schema's \`schemaVersion\`, no numeric \`id\`,
95
+ and every field Grafana's schema requires (link defaults, an empty
96
+ \`annotations.list\`, \`fieldConfig.overrides\`).
97
+ - Panels without a position are laid out in declaration order on the 24-column
98
+ grid. Ids and query \`refId\`s are assigned in order unless set.
99
+ - A declared \`Datasource\` is written as \`{ type, uid }\` wherever it is
100
+ referenced, and as its uid inside another datasource's \`jsonData\`.
101
+ - \`chant build\` rewrites JSON files with sorted keys; Grafana does not care
102
+ about key order.
103
+ - The dashboard JSON carries no ownership marker. Grafana keeps who manages a
104
+ dashboard in the resource's metadata on its \`dashboard.grafana.app\` API,
105
+ not in the JSON: for a provisioned dashboard, the name of the provider that
106
+ loaded it, which is \`chant\` unless a \`DashboardProvider\` names another.
107
+ \`chant lifecycle diff --live\` reads that back (see
108
+ [Drift and Live Export](../observing/)).
109
+ `;
110
+ export async function generateDocs(opts) {
111
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
112
+ const config = {
113
+ name: "grafana",
114
+ displayName: "Grafana",
115
+ description: "Typed Grafana dashboards, panels, queries and datasources, built to dashboard JSON and provisioning files",
116
+ distDir: join(pkgDir, "dist"),
117
+ outDir: join(pkgDir, "docs"),
118
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/grafana/",
119
+ overview,
120
+ outputFormat,
121
+ serviceFromType,
122
+ srcDir: join(pkgDir, "src"),
123
+ examplesDir: join(pkgDir, "examples"),
124
+ };
125
+ const result = docsPipeline(config);
126
+ writeDocsSite(config, result);
127
+ if (opts?.verbose) {
128
+ console.error(`Generated ${result.pages.size} documentation pages`);
129
+ }
130
+ }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for `npm run generate` in lexicon-grafana.
4
+ */
5
+ import { generate, writeGeneratedFiles } from "./generate.js";
6
+ const result = await generate({ verbose: true });
7
+ writeGeneratedFiles(result);
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The grafana lexicon's generate step, offline and deterministic.
3
+ *
4
+ * 1. `src/schema/<name>.gen.ts` from each vendored schema in `src/spec/schemas/`
5
+ * (see `src/pin.ts`). These are committed, and a test fails when they
6
+ * drift from what this step would write.
7
+ * 2. `src/spec/schemas.gen.ts`, the same schemas with the overlay applied, as
8
+ * JSON text. Validation (GRAF107, the `/validation` subpath) reads them
9
+ * from there rather than from the files, so it needs no filesystem access
10
+ * and works bundled. Committed and checked the same way.
11
+ * 3. `src/generated/lexicon-grafana.json`, the registry of entity classes
12
+ * the package exports, derived from the catalog so the packaged
13
+ * `dist/meta.json` cannot disagree with the classes.
14
+ */
15
+ import { dirname, join } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import { mkdirSync, writeFileSync } from "fs";
18
+ import { SCHEMA_NAMES } from "../pin.js";
19
+ import { digestMismatches, loadSchema } from "../spec/schemas.js";
20
+ import { schemaModule } from "./schema-types.js";
21
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
22
+ /** The dashboard schema's own default `schemaVersion`, the one the serializer emits. */
23
+ function dashboardExtras(schema) {
24
+ const defs = schema.definitions;
25
+ const v = defs.Dashboard?.properties?.schemaVersion?.default;
26
+ if (typeof v !== "number")
27
+ throw new Error("grafana: dashboard schema has no default schemaVersion");
28
+ return ["/** The dashboard `schemaVersion` the pinned schema defaults to, and the one chant emits. */", `export const DASHBOARD_SCHEMA_VERSION = ${v};`];
29
+ }
30
+ /** `src/spec/schemas.gen.ts`: each schema validation uses, overlay applied, as one line of JSON text. */
31
+ export function schemaDataModule() {
32
+ const lines = [
33
+ "// Code generated by chant generate. DO NOT EDIT.",
34
+ "// The vendored Grafana schemas (src/spec/schemas/, see src/pin.ts) with the",
35
+ "// correction overlay (src/spec/overlay/) applied, as JSON text, for",
36
+ "// src/schema-validate.ts. A module rather than a file read, so validation",
37
+ "// needs no filesystem access and survives bundling; text rather than object",
38
+ "// literals, so loading it costs little until a schema is parsed.",
39
+ "",
40
+ 'import type { SchemaName } from "../pin.js";',
41
+ "",
42
+ "export const SCHEMA_JSON: Readonly<Record<SchemaName, string>> = {",
43
+ ];
44
+ for (const name of SCHEMA_NAMES) {
45
+ lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(JSON.stringify(loadSchema(name)))},`);
46
+ }
47
+ lines.push("};", "");
48
+ return lines.join("\n");
49
+ }
50
+ /** Every generated schema module, keyed by path relative to the package root. */
51
+ export function schemaModules() {
52
+ const out = {};
53
+ for (const name of SCHEMA_NAMES) {
54
+ const schema = loadSchema(name);
55
+ const extra = name === "dashboard" ? dashboardExtras(schema) : [];
56
+ out[`src/schema/${name}.gen.ts`] = schemaModule(name, schema, extra).source;
57
+ }
58
+ out["src/spec/schemas.gen.ts"] = schemaDataModule();
59
+ return out;
60
+ }
61
+ /** The packaged types entry re-exports the compiled declarations. */
62
+ const TYPES_DTS = `// The grafana lexicon's entity classes are hand-written in src/, with panel
63
+ // and query types generated into src/schema/; this entry re-exports them.
64
+ export * from "../index.js";
65
+ `;
66
+ export async function generate(opts) {
67
+ const bad = digestMismatches();
68
+ if (bad.length > 0) {
69
+ throw new Error(`grafana: vendored schemas do not match GRAFANA_SCHEMA_PIN: ${bad.map((b) => `${b.name} (${b.actual.slice(0, 12)}…)`).join(", ")}. ` +
70
+ "Run `just fetch-schemas` and update src/pin.ts.");
71
+ }
72
+ const modules = schemaModules();
73
+ for (const [rel, source] of Object.entries(modules)) {
74
+ mkdirSync(dirname(join(pkgDir, rel)), { recursive: true });
75
+ writeFileSync(join(pkgDir, rel), source);
76
+ }
77
+ const { BUILTIN_CATALOG, lexiconRegistry } = await import("../catalog.js");
78
+ const lexiconJSON = `${JSON.stringify(lexiconRegistry(), null, 2)}\n`;
79
+ if (opts?.verbose) {
80
+ console.error(`grafana: ${Object.keys(modules).length} schema modules, ${BUILTIN_CATALOG.length} entity classes`);
81
+ }
82
+ return {
83
+ lexiconJSON,
84
+ typesDTS: TYPES_DTS,
85
+ indexTS: "",
86
+ resources: BUILTIN_CATALOG.length,
87
+ properties: 0,
88
+ enums: 0,
89
+ warnings: [],
90
+ };
91
+ }
92
+ /** Write the registry to src/generated/lexicon-grafana.json, for tools that read it from there. */
93
+ export function writeGeneratedFiles(result) {
94
+ const dir = join(pkgDir, "src", "generated");
95
+ mkdirSync(dir, { recursive: true });
96
+ writeFileSync(join(dir, "lexicon-grafana.json"), result.lexiconJSON);
97
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * grafana lexicon packaging, delegating to core's packagePipeline.
3
+ */
4
+ import { readFileSync } from "fs";
5
+ import { join, dirname } from "path";
6
+ import { moduleSourceDir } from "@intentius/chant/module-source";
7
+ import { packagePipeline, collectSkills, } from "@intentius/chant/codegen/package";
8
+ import { generate } from "./generate.js";
9
+ import { grafanaSkills } from "../skill-defs.js";
10
+ // src/codegen/package.ts -> src/
11
+ const srcDir = dirname(moduleSourceDir(import.meta.url));
12
+ export async function packageLexicon(opts = {}) {
13
+ const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
14
+ return packagePipeline({
15
+ generate: (genOpts) => generate({ verbose: genOpts.verbose, force: genOpts.force }),
16
+ buildManifest: () => ({
17
+ name: "grafana",
18
+ version: pkgJson.version ?? "0.0.1",
19
+ chantVersion: ">=0.1.0",
20
+ namespace: "Grafana",
21
+ intrinsics: [],
22
+ pseudoParameters: {},
23
+ }),
24
+ srcDir,
25
+ collectSkills: () => collectSkills(grafanaSkills()),
26
+ version: pkgJson.version ?? "0.0.1",
27
+ }, opts);
28
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * JSON Schema (draft-07, the subset cog emits) to TypeScript declarations.
3
+ *
4
+ * Each vendored schema, with its correction overlay applied, becomes one
5
+ * module under `src/schema/`: one exported type per entry in `definitions`,
6
+ * with the schema's descriptions as doc comments. `required` is kept, so the types say what the schema says; the
7
+ * entity props wrap panel options in `DeepPartial` where Grafana fills the
8
+ * rest in on import.
9
+ */
10
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
11
+ /**
12
+ * Keys written quoted even though they are identifiers. The repo's egress
13
+ * scan (test/egress-catalogue.ts) reads a bare `fetch` as a call to the
14
+ * global, and the dashboard schema has an action option named `fetch`.
15
+ */
16
+ const QUOTED_KEYS = new Set(["fetch"]);
17
+ /** `dataquery` -> `Dataquery`: every exported type starts upper-case. */
18
+ export function typeName(defName) {
19
+ return defName.charAt(0).toUpperCase() + defName.slice(1);
20
+ }
21
+ function refName(ref) {
22
+ const m = /^#\/definitions\/(.+)$/.exec(ref);
23
+ if (!m)
24
+ throw new Error(`schema-types: only local #/definitions refs are supported, got "${ref}"`);
25
+ return typeName(m[1]);
26
+ }
27
+ function literal(value) {
28
+ return JSON.stringify(value);
29
+ }
30
+ function paren(t) {
31
+ return /[|&]/.test(t) && !/^\{[\s\S]*\}$/.test(t) ? `(${t})` : t;
32
+ }
33
+ function union(parts) {
34
+ const uniq = [...new Set(parts)];
35
+ if (uniq.includes("unknown"))
36
+ return "unknown";
37
+ return uniq.length === 0 ? "never" : uniq.join(" | ");
38
+ }
39
+ function docComment(node, indent) {
40
+ const lines = [];
41
+ const text = typeof node.description === "string" ? node.description.trim() : "";
42
+ const deprecated = node.deprecated === true || typeof node["x-deprecation-message"] === "string";
43
+ if (!text && !deprecated)
44
+ return lines;
45
+ lines.push(`${indent}/**`);
46
+ for (const raw of text ? text.replace(/\*\//g, "*\\/").split("\n") : []) {
47
+ const l = raw.trimEnd();
48
+ lines.push(`${indent} *${l ? ` ${l}` : ""}`);
49
+ }
50
+ if (deprecated) {
51
+ const msg = typeof node["x-deprecation-message"] === "string" ? ` ${String(node["x-deprecation-message"])}` : "";
52
+ lines.push(`${indent} * @deprecated${msg.replace(/\*\//g, "*\\/")}`);
53
+ }
54
+ lines.push(`${indent} */`);
55
+ return lines;
56
+ }
57
+ function objectType(node, indent) {
58
+ const props = node.properties ?? {};
59
+ const required = new Set(node.required ?? []);
60
+ const inner = `${indent} `;
61
+ const lines = ["{"];
62
+ for (const [key, child] of Object.entries(props)) {
63
+ lines.push(...docComment(child, inner));
64
+ const k = IDENT.test(key) && !QUOTED_KEYS.has(key) ? key : JSON.stringify(key);
65
+ lines.push(`${inner}${k}${required.has(key) ? "" : "?"}: ${tsType(child, inner)};`);
66
+ }
67
+ const ap = node.additionalProperties;
68
+ if (ap !== undefined && ap !== false) {
69
+ const t = ap === true || Object.keys(ap).length === 0 ? "unknown" : tsType(ap, inner);
70
+ lines.push(`${inner}[key: string]: ${Object.keys(props).length > 0 ? "unknown" : t};`);
71
+ }
72
+ lines.push(`${indent}}`);
73
+ return lines.length === 2 ? "Record<string, never>" : lines.join("\n");
74
+ }
75
+ /**
76
+ * A branch that only constrains which keys are present (`{ required: [...] }`),
77
+ * like the overlay's "a panel has a `type` or a `libraryPanel`". It says
78
+ * nothing about the shape, so the type ignores it and validation enforces it.
79
+ */
80
+ function constraintOnly(branches) {
81
+ return Array.isArray(branches) && branches.every((b) => Object.keys(b).every((k) => k === "required"));
82
+ }
83
+ /** The TypeScript type for one schema node. */
84
+ export function tsType(node, indent = "") {
85
+ if (node === undefined || node === true)
86
+ return "unknown";
87
+ if (node === false)
88
+ return "never";
89
+ if (node.$ref)
90
+ return refName(node.$ref);
91
+ if (node.const !== undefined)
92
+ return literal(node.const);
93
+ if (Array.isArray(node.enum))
94
+ return union(node.enum.map(literal));
95
+ if (node.oneOf && !constraintOnly(node.oneOf))
96
+ return union(node.oneOf.map((n) => paren(tsType(n, indent))));
97
+ if (node.anyOf && !constraintOnly(node.anyOf))
98
+ return union(node.anyOf.map((n) => paren(tsType(n, indent))));
99
+ if (node.allOf)
100
+ return node.allOf.map((n) => paren(tsType(n, indent))).join(" & ");
101
+ const types = Array.isArray(node.type) ? node.type : node.type ? [node.type] : [];
102
+ if (types.length === 0)
103
+ return node.properties ? objectType(node, indent) : "unknown";
104
+ return union(types.map((t) => {
105
+ switch (t) {
106
+ case "string":
107
+ return "string";
108
+ case "number":
109
+ case "integer":
110
+ return "number";
111
+ case "boolean":
112
+ return "boolean";
113
+ case "null":
114
+ return "null";
115
+ case "array": {
116
+ if (Array.isArray(node.items))
117
+ return `[${node.items.map((n) => tsType(n, indent)).join(", ")}]`;
118
+ return `${paren(tsType(node.items, indent))}[]`;
119
+ }
120
+ case "object": {
121
+ if (node.properties)
122
+ return objectType(node, indent);
123
+ const ap = node.additionalProperties;
124
+ if (ap === false)
125
+ return "Record<string, never>";
126
+ if (ap === undefined || ap === true || Object.keys(ap).length === 0)
127
+ return "Record<string, unknown>";
128
+ return `Record<string, ${tsType(ap, indent)}>`;
129
+ }
130
+ default:
131
+ return "unknown";
132
+ }
133
+ }));
134
+ }
135
+ /** One schema file to one TypeScript module. */
136
+ export function schemaModule(name, schema, extra = []) {
137
+ const defs = (schema.definitions ?? {});
138
+ const out = [
139
+ "// Code generated by chant generate from the vendored Grafana schema",
140
+ `// src/spec/schemas/${name}.jsonschema.json (see src/pin.ts) and its correction`,
141
+ "// overlay in src/spec/overlay/ (see src/spec/overlay.ts). DO NOT EDIT.",
142
+ "/* eslint-disable */",
143
+ "",
144
+ ];
145
+ const types = [];
146
+ for (const [defName, def] of Object.entries(defs)) {
147
+ const tn = typeName(defName);
148
+ types.push(tn);
149
+ out.push(...docComment(def, ""));
150
+ const t = tsType(def, "");
151
+ if (t.startsWith("{"))
152
+ out.push(`export interface ${tn} ${t}`);
153
+ else
154
+ out.push(`export type ${tn} = ${t};`);
155
+ out.push("");
156
+ }
157
+ out.push(...extra);
158
+ return { source: `${out.join("\n").replace(/\n+$/, "")}\n`, types };
159
+ }