@marble-sh/backstage-plugin-grafana-backend 1.0.2 → 1.2.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/CHANGELOG.md +57 -0
- package/README.md +72 -21
- package/config.schema.json +34 -0
- package/dist/grafana/config.cjs.js +4 -1
- package/dist/grafana/config.cjs.js.map +1 -1
- package/dist/index.d.ts +66 -4
- package/dist/plugin.cjs.js +5 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/service/GrafanaService.cjs.js +52 -1
- package/dist/service/GrafanaService.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +47 -0
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# @marble-sh/backstage-plugin-grafana-backend
|
|
2
2
|
|
|
3
|
+
## 1.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- a31b91d: Added: the dashboards tab now renders real graphs and the alerts tab a live
|
|
8
|
+
detail table. The backend gained read-only panel routes
|
|
9
|
+
(`GET …/dashboards/:uid/panels` and `GET …/panels/:panelId/data?from&to`)
|
|
10
|
+
that read a dashboard's model, resolve its template variables' current
|
|
11
|
+
values, query the panel targets through Grafana's `/api/ds/query`, and
|
|
12
|
+
return normalized time series — gated by the new `grafana.allowPanelQueries`
|
|
13
|
+
flag and cached per `grafana.panelDataCacheTtl` (default 30s). The frontend
|
|
14
|
+
draws `timeseries`/`graph` panels as charts and `stat`/`gauge`/`singlestat`
|
|
15
|
+
panels as value tiles, per-dashboard and lazily, with a time-range picker
|
|
16
|
+
and refresh. Alerts are enriched with rule uid (deep links), health,
|
|
17
|
+
active-since, active instance count, dashboard/panel links, and the
|
|
18
|
+
`summary` annotation. `GrafanaClient`/`GrafanaService` gained _optional_
|
|
19
|
+
`getPanels`/`getPanelData` members, so existing custom implementations
|
|
20
|
+
remain compatible.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [a31b91d]
|
|
25
|
+
- @marble-sh/backstage-plugin-grafana-common@1.2.0
|
|
26
|
+
- @marble-sh/backstage-plugin-grafana-node@1.2.0
|
|
27
|
+
|
|
28
|
+
## 1.1.0
|
|
29
|
+
|
|
30
|
+
### Minor Changes
|
|
31
|
+
|
|
32
|
+
- 476051b: Catalog discovery no longer leaves the `defaultOwner` relation dangling, and
|
|
33
|
+
every discovered dashboard `Resource` now shows exactly its own dashboard.
|
|
34
|
+
|
|
35
|
+
- The entity provider creates a placeholder `Group` (`spec.type: virtual`) for
|
|
36
|
+
`defaultOwner` while no other catalog source defines that ref; a definition
|
|
37
|
+
from any other source (e.g. a hand-written catalog-info.yaml) always takes
|
|
38
|
+
precedence. Disable with `grafana.catalog.emitOwnerGroup: false` — also the
|
|
39
|
+
handover switch for replacing an existing placeholder with your own
|
|
40
|
+
definition (see the module README).
|
|
41
|
+
- New `grafana/dashboard-uid` annotation (exact, case-sensitive uid match),
|
|
42
|
+
supported end-to-end: `getDashboardUid` in `-common`, `DashboardFilter.uid`
|
|
43
|
+
in `-node`, a `uid` query parameter on the backend dashboard routes,
|
|
44
|
+
`ListDashboardsRequest.uid` + `DashboardsCard` support in the frontend, and
|
|
45
|
+
emitted by catalog discovery on every dashboard `Resource`.
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- d091ba9: Reject a non-numeric Grafana Cloud `stackId` at config-read time with an
|
|
50
|
+
error that explains the numeric-id-vs-slug distinction and how to look the id
|
|
51
|
+
up, instead of silently deriving an invalid App Platform namespace that fails
|
|
52
|
+
every refresh with 403 "invalid namespace". Documented the numeric requirement
|
|
53
|
+
and both lookup methods (Cloud portal URL, `GET /api/frontend/settings`) in
|
|
54
|
+
the backend README and the config schema.
|
|
55
|
+
- Updated dependencies [476051b]
|
|
56
|
+
- Updated dependencies [d091ba9]
|
|
57
|
+
- @marble-sh/backstage-plugin-grafana-common@1.1.0
|
|
58
|
+
- @marble-sh/backstage-plugin-grafana-node@1.1.0
|
|
59
|
+
|
|
3
60
|
## 1.0.2
|
|
4
61
|
|
|
5
62
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -18,7 +18,14 @@ credentials stay in the backend.
|
|
|
18
18
|
`apis.dashboards: legacy-search` — from the classic `/api/search` endpoint
|
|
19
19
|
for older Grafana versions. Folder titles and links are resolved for both.
|
|
20
20
|
- Reads alert rules and their live state from the stable Grafana-managed
|
|
21
|
-
Prometheus rules API (`/api/prometheus/grafana/api/v1/rules`)
|
|
21
|
+
Prometheus rules API (`/api/prometheus/grafana/api/v1/rules`), including
|
|
22
|
+
rule uid (for deep links), health, active-since time, active instance
|
|
23
|
+
count, dashboard/panel links, and the `summary` annotation.
|
|
24
|
+
- Serves **panel listings and live panel data**: reads a dashboard's model,
|
|
25
|
+
substitutes its template variables' current values, queries the panel's
|
|
26
|
+
targets through `POST /api/ds/query`, and returns plain normalized time
|
|
27
|
+
series — so the frontend can draw real graphs without ever talking to
|
|
28
|
+
Grafana. See [ADR 0006](../../docs/adr/0006-panel-graphs-via-backend-query-proxy.md).
|
|
22
29
|
- Works with both **Grafana Cloud** and **self-hosted** Grafana, using a
|
|
23
30
|
service-account token.
|
|
24
31
|
- Supports **multiple instances**, each addressable by name.
|
|
@@ -61,9 +68,12 @@ grafana:
|
|
|
61
68
|
store: cache
|
|
62
69
|
cacheTtl: { minutes: 15 }
|
|
63
70
|
|
|
64
|
-
# Behavior flags (
|
|
71
|
+
# Behavior flags (all default to true; see "Behavior flags" below).
|
|
65
72
|
allowOnDemandRefresh: true
|
|
66
73
|
fetchOnDemand: true
|
|
74
|
+
allowPanelQueries: true
|
|
75
|
+
# How long live panel data is cached (see "Behavior flags" below).
|
|
76
|
+
panelDataCacheTtl: { seconds: 30 }
|
|
67
77
|
|
|
68
78
|
# Optional background refresh. Omit to fetch lazily on request instead.
|
|
69
79
|
schedule:
|
|
@@ -95,7 +105,7 @@ grafana:
|
|
|
95
105
|
| `token` | yes | Service-account token used as a Bearer token. Read-only (Viewer) permissions are enough. Marked secret. |
|
|
96
106
|
| `title` | no | Human-readable title. Defaults to `name`. |
|
|
97
107
|
| `namespace` | no | App Platform namespace. Defaults to `default` (self-hosted) or `stacks-<stackId>` (cloud). |
|
|
98
|
-
| `stackId` | no | Grafana Cloud stack id, used to derive the namespace.
|
|
108
|
+
| `stackId` | no | **Numeric** Grafana Cloud stack id (not the stack slug), used to derive the namespace (see below). |
|
|
99
109
|
| `apis` | no | Override or disable the API used per data type (see below). |
|
|
100
110
|
| `resolveFolders` | no | `false` skips the `/api/folders` folder lookup (see below). Defaults to `true`. |
|
|
101
111
|
|
|
@@ -131,8 +141,8 @@ Every state, per data type:
|
|
|
131
141
|
|
|
132
142
|
### Behavior flags
|
|
133
143
|
|
|
134
|
-
|
|
135
|
-
`true`; each can be set independently.
|
|
144
|
+
Three top-level flags control when the backend talks to Grafana. All default
|
|
145
|
+
to `true`; each can be set independently.
|
|
136
146
|
|
|
137
147
|
- **`allowOnDemandRefresh`** — may API callers force live reads?
|
|
138
148
|
|
|
@@ -144,19 +154,29 @@ Two top-level flags control when the backend talks to Grafana. Both default to
|
|
|
144
154
|
routes respond `403 NotAllowedError`. The scheduled refresh is unaffected.
|
|
145
155
|
|
|
146
156
|
- **`fetchOnDemand`** — does a store miss trigger a live read?
|
|
157
|
+
|
|
147
158
|
- `true` (default): a miss fetches from Grafana on the spot and stores the
|
|
148
159
|
snapshot.
|
|
149
160
|
- `false`: a miss returns empty results and stores nothing; data appears
|
|
150
161
|
once any refresh runs.
|
|
151
162
|
|
|
163
|
+
- **`allowPanelQueries`** — are the panel routes served?
|
|
164
|
+
- `true` (default): `GET …/dashboards/:uid/panels` and
|
|
165
|
+
`GET …/panels/:panelId/data` read live from Grafana (the dashboard model
|
|
166
|
+
plus the `/api/ds/query` datasource API), briefly cached per
|
|
167
|
+
`grafana.panelDataCacheTtl` (default 30 seconds) to absorb bursts.
|
|
168
|
+
- `false`: both routes respond `403`. Panel data cannot come from the
|
|
169
|
+
snapshot store, so deployments that need strictly schedule-only Grafana
|
|
170
|
+
traffic should set this alongside the other two flags.
|
|
171
|
+
|
|
152
172
|
All four combinations:
|
|
153
173
|
|
|
154
|
-
| `allowOnDemandRefresh` | `fetchOnDemand` | Resulting behavior
|
|
155
|
-
| ---------------------- | --------------- |
|
|
156
|
-
| `true` | `true` | (Default.) Misses fetch lazily; users may force refreshes.
|
|
157
|
-
| `true` | `false` | Misses return empty, but an explicit `?refresh=true` or `POST …/refresh` still reads live and fills the store.
|
|
158
|
-
| `false` | `true` | Users cannot force refreshes, but a cold store still fills itself lazily on first read.
|
|
159
|
-
| `false` | `false` | Grafana is contacted **only** by the schedule (configure one, or the API serves empty forever).
|
|
174
|
+
| `allowOnDemandRefresh` | `fetchOnDemand` | Resulting behavior |
|
|
175
|
+
| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
176
|
+
| `true` | `true` | (Default.) Misses fetch lazily; users may force refreshes. |
|
|
177
|
+
| `true` | `false` | Misses return empty, but an explicit `?refresh=true` or `POST …/refresh` still reads live and fills the store. |
|
|
178
|
+
| `false` | `true` | Users cannot force refreshes, but a cold store still fills itself lazily on first read. |
|
|
179
|
+
| `false` | `false` | Grafana is contacted **only** by the schedule (configure one, or the API serves empty forever). With `allowPanelQueries: false` as well, Grafana traffic is fully deterministic. |
|
|
160
180
|
|
|
161
181
|
### Deriving the namespace
|
|
162
182
|
|
|
@@ -166,31 +186,53 @@ Grafana's App Platform APIs are namespaced:
|
|
|
166
186
|
- Self-hosted, other organizations → `org-<id>` (set `namespace` explicitly)
|
|
167
187
|
- Grafana Cloud → `stacks-<stackId>` (set `stackId`, or `namespace` directly)
|
|
168
188
|
|
|
189
|
+
`stackId` is the **numeric** stack id, not the stack slug — for a stack at
|
|
190
|
+
`myorg.grafana.net`, `myorg` is the slug, not the id. A non-numeric `stackId`
|
|
191
|
+
is rejected at startup. Two ways to find the id:
|
|
192
|
+
|
|
193
|
+
- In the Grafana Cloud portal, open the stack; the URL is
|
|
194
|
+
`grafana.com/orgs/<org>/stacks/<id>`.
|
|
195
|
+
- Ask the instance itself, using the same service-account token this plugin
|
|
196
|
+
uses:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
curl -s -H "Authorization: Bearer $TOKEN" \
|
|
200
|
+
https://<slug>.grafana.net/api/frontend/settings | jq -r .namespace
|
|
201
|
+
# → "stacks-1216502" — the number is the stackId
|
|
202
|
+
```
|
|
203
|
+
|
|
169
204
|
## REST API
|
|
170
205
|
|
|
171
206
|
All routes are mounted under `/api/grafana` and (except `/health`) require a
|
|
172
207
|
valid Backstage credential.
|
|
173
208
|
|
|
174
|
-
| Method | Path
|
|
175
|
-
| ------ |
|
|
176
|
-
| GET | `/health`
|
|
177
|
-
| GET | `/instances`
|
|
178
|
-
| GET | `/instances/:name/dashboards`
|
|
179
|
-
| GET | `/instances/:name/
|
|
180
|
-
|
|
|
181
|
-
| GET | `/
|
|
182
|
-
|
|
|
183
|
-
|
|
|
209
|
+
| Method | Path | Description |
|
|
210
|
+
| ------ | ------------------------------------------------------- | ---------------------------------------------------------- |
|
|
211
|
+
| GET | `/health` | Health check (unauthenticated). |
|
|
212
|
+
| GET | `/instances` | List configured instances. |
|
|
213
|
+
| GET | `/instances/:name/dashboards` | Dashboards for one instance. |
|
|
214
|
+
| GET | `/instances/:name/dashboards/:uid/panels` | The panels of one dashboard (live read). |
|
|
215
|
+
| GET | `/instances/:name/dashboards/:uid/panels/:panelId/data` | Normalized time series for one panel (live query). |
|
|
216
|
+
| GET | `/instances/:name/alerts` | Alerts for one instance. |
|
|
217
|
+
| POST | `/instances/:name/refresh` | Force a refresh of one instance. |
|
|
218
|
+
| GET | `/dashboards` | Dashboards across all instances (or `?instance=` for one). |
|
|
219
|
+
| GET | `/alerts` | Alerts across all instances (or `?instance=` for one). |
|
|
220
|
+
| POST | `/refresh` | Force a refresh of all instances. |
|
|
184
221
|
|
|
185
222
|
### Query parameters
|
|
186
223
|
|
|
187
224
|
- `tag` — repeatable; only dashboards carrying **all** given tags are returned.
|
|
188
225
|
- `query` — comma-separated, case-insensitive title substrings; dashboards
|
|
189
226
|
matching **any** value are returned.
|
|
227
|
+
- `uid` — only the dashboard with exactly this uid (case-sensitive) is
|
|
228
|
+
returned; combines with the other filters.
|
|
190
229
|
- `labelSelector` — `key=value,key2=value2`; only alerts matching **all** pairs.
|
|
191
230
|
- `instance` — (on `/dashboards` and `/alerts`) restrict to a single instance.
|
|
192
231
|
- `refresh` — `true` or `1` (or the bare flag) to bypass the store and read
|
|
193
232
|
live from Grafana.
|
|
233
|
+
- `from` / `to` — (on the panel data route) the query range, as Grafana time
|
|
234
|
+
expressions: `now`, `now-<n><s|m|h|d|w>`, or epoch milliseconds. Default
|
|
235
|
+
`now-6h` … `now`.
|
|
194
236
|
|
|
195
237
|
Example:
|
|
196
238
|
|
|
@@ -211,6 +253,15 @@ Each request resolves to a per-instance snapshot (all dashboards + all alerts):
|
|
|
211
253
|
Because filtering happens after retrieval, a single cached snapshot serves many
|
|
212
254
|
entities with different selectors, keeping Grafana API traffic low.
|
|
213
255
|
|
|
256
|
+
Panel listings and panel data are different: they are inherently live (a graph
|
|
257
|
+
of a stale range is wrong, not cached), so they bypass the snapshot store
|
|
258
|
+
entirely. Instead they are kept in the cache service for a short
|
|
259
|
+
`panelDataCacheTtl` (default 30 seconds) keyed by instance, dashboard, panel,
|
|
260
|
+
and range — enough to absorb a dashboard opening (which queries every panel at
|
|
261
|
+
once) and several viewers of the same entity, without making graphs stale. The
|
|
262
|
+
Grafana-side dashboard-model read is additionally deduplicated in the client,
|
|
263
|
+
so one burst reads the model once.
|
|
264
|
+
|
|
214
265
|
## Local development
|
|
215
266
|
|
|
216
267
|
```sh
|
package/config.schema.json
CHANGED
|
@@ -51,6 +51,40 @@
|
|
|
51
51
|
"type": "boolean",
|
|
52
52
|
"description": "Whether a store miss triggers a live read from Grafana.\n\n - `true` (default): when no snapshot is stored for an instance, the backend fetches from Grafana on the spot and stores the result. - `false`: store misses return empty results without contacting Grafana. Data appears once a refresh runs — the schedule, the `POST …/refresh` routes, or a `refresh=true` read (the latter two only if `allowOnDemandRefresh` permits them). With both flags `false`, Grafana is contacted exclusively by the schedule."
|
|
53
53
|
},
|
|
54
|
+
"allowPanelQueries": {
|
|
55
|
+
"type": "boolean",
|
|
56
|
+
"description": "Whether the panel routes are served.\n\n - `true` (default): `GET …/dashboards/:uid/panels` and `GET …/panels/:panelId/data` read live from Grafana (the dashboard model and the datasource query API), briefly cached per `panelDataCacheTtl`. - `false`: both routes respond `403`. Set this together with `allowOnDemandRefresh: false` and `fetchOnDemand: false` when Grafana traffic must be strictly schedule-only — panel data cannot be served from the snapshot store."
|
|
57
|
+
},
|
|
58
|
+
"panelDataCacheTtl": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"years": {
|
|
62
|
+
"type": "number"
|
|
63
|
+
},
|
|
64
|
+
"months": {
|
|
65
|
+
"type": "number"
|
|
66
|
+
},
|
|
67
|
+
"weeks": {
|
|
68
|
+
"type": "number"
|
|
69
|
+
},
|
|
70
|
+
"days": {
|
|
71
|
+
"type": "number"
|
|
72
|
+
},
|
|
73
|
+
"hours": {
|
|
74
|
+
"type": "number"
|
|
75
|
+
},
|
|
76
|
+
"minutes": {
|
|
77
|
+
"type": "number"
|
|
78
|
+
},
|
|
79
|
+
"seconds": {
|
|
80
|
+
"type": "number"
|
|
81
|
+
},
|
|
82
|
+
"milliseconds": {
|
|
83
|
+
"type": "number"
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"description": "Time-to-live for cached panel listings and panel data. Short by design: it exists to absorb bursts (opening a dashboard queries every panel at once), not to make graphs stale. Defaults to 30 seconds."
|
|
87
|
+
},
|
|
54
88
|
"schedule": {
|
|
55
89
|
"type": "object",
|
|
56
90
|
"properties": {
|
|
@@ -5,6 +5,7 @@ var errors = require('@backstage/errors');
|
|
|
5
5
|
var backstagePluginGrafanaNode = require('@marble-sh/backstage-plugin-grafana-node');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_CACHE_TTL = { minutes: 15 };
|
|
8
|
+
const DEFAULT_PANEL_DATA_CACHE_TTL = { seconds: 30 };
|
|
8
9
|
function readGrafanaConfig(rootConfig) {
|
|
9
10
|
const instances = backstagePluginGrafanaNode.readGrafanaInstances(rootConfig);
|
|
10
11
|
const config = rootConfig.getOptionalConfig("grafana");
|
|
@@ -21,7 +22,9 @@ function readGrafanaConfig(rootConfig) {
|
|
|
21
22
|
cacheTtl: config?.getOptional("cacheTtl") ?? DEFAULT_CACHE_TTL,
|
|
22
23
|
schedule: scheduleConfig ? backendPluginApi.readSchedulerServiceTaskScheduleDefinitionFromConfig(scheduleConfig) : void 0,
|
|
23
24
|
allowOnDemandRefresh: config?.getOptionalBoolean("allowOnDemandRefresh") ?? true,
|
|
24
|
-
fetchOnDemand: config?.getOptionalBoolean("fetchOnDemand") ?? true
|
|
25
|
+
fetchOnDemand: config?.getOptionalBoolean("fetchOnDemand") ?? true,
|
|
26
|
+
allowPanelQueries: config?.getOptionalBoolean("allowPanelQueries") ?? true,
|
|
27
|
+
panelDataCacheTtl: config?.getOptional("panelDataCacheTtl") ?? DEFAULT_PANEL_DATA_CACHE_TTL
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.cjs.js","sources":["../../src/grafana/config.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport {\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { InputError } from '@backstage/errors';\nimport { HumanDuration } from '@backstage/types';\nimport {\n GrafanaInstanceConfig,\n readGrafanaInstances,\n} from '@marble-sh/backstage-plugin-grafana-node';\n\nexport type {\n GrafanaInstanceConfig,\n GrafanaInstanceApis,\n} from '@marble-sh/backstage-plugin-grafana-node';\n\n/**\n * Where fetched data is stored between refreshes.\n *\n * @public\n */\nexport type GrafanaStoreKind = 'cache' | 'database';\n\n/**\n * The fully-resolved backend configuration.\n *\n * @public\n */\nexport type GrafanaBackendConfig = {\n instances: GrafanaInstanceConfig[];\n store: GrafanaStoreKind;\n cacheTtl: HumanDuration;\n schedule?: SchedulerServiceTaskScheduleDefinition;\n /** Whether callers may force live reads (`?refresh=true`, `POST /refresh`). */\n allowOnDemandRefresh: boolean;\n /** Whether a store miss triggers a live Grafana read. */\n fetchOnDemand: boolean;\n};\n\nconst DEFAULT_CACHE_TTL: HumanDuration = { minutes: 15 };\n\n/**\n * Reads and validates the `grafana` section of the app configuration into a\n * fully-resolved {@link GrafanaBackendConfig}.\n *\n * @public\n */\nexport function readGrafanaConfig(rootConfig: Config): GrafanaBackendConfig {\n const instances = readGrafanaInstances(rootConfig);\n const config = rootConfig.getOptionalConfig('grafana');\n\n const store =\n (config?.getOptionalString('store') as GrafanaStoreKind | undefined) ??\n 'cache';\n if (store !== 'cache' && store !== 'database') {\n throw new InputError(\n `Invalid grafana.store '${store}', expected 'cache' or 'database'`,\n );\n }\n\n const scheduleConfig = config?.getOptionalConfig('schedule');\n\n return {\n instances,\n store,\n cacheTtl:\n (config?.getOptional('cacheTtl') as HumanDuration | undefined) ??\n DEFAULT_CACHE_TTL,\n schedule: scheduleConfig\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(scheduleConfig)\n : undefined,\n allowOnDemandRefresh:\n config?.getOptionalBoolean('allowOnDemandRefresh') ?? true,\n fetchOnDemand: config?.getOptionalBoolean('fetchOnDemand') ?? true,\n };\n}\n"],"names":["readGrafanaInstances","InputError","readSchedulerServiceTaskScheduleDefinitionFromConfig"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"config.cjs.js","sources":["../../src/grafana/config.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Config } from '@backstage/config';\nimport {\n readSchedulerServiceTaskScheduleDefinitionFromConfig,\n SchedulerServiceTaskScheduleDefinition,\n} from '@backstage/backend-plugin-api';\nimport { InputError } from '@backstage/errors';\nimport { HumanDuration } from '@backstage/types';\nimport {\n GrafanaInstanceConfig,\n readGrafanaInstances,\n} from '@marble-sh/backstage-plugin-grafana-node';\n\nexport type {\n GrafanaInstanceConfig,\n GrafanaInstanceApis,\n} from '@marble-sh/backstage-plugin-grafana-node';\n\n/**\n * Where fetched data is stored between refreshes.\n *\n * @public\n */\nexport type GrafanaStoreKind = 'cache' | 'database';\n\n/**\n * The fully-resolved backend configuration.\n *\n * @public\n */\nexport type GrafanaBackendConfig = {\n instances: GrafanaInstanceConfig[];\n store: GrafanaStoreKind;\n cacheTtl: HumanDuration;\n schedule?: SchedulerServiceTaskScheduleDefinition;\n /** Whether callers may force live reads (`?refresh=true`, `POST /refresh`). */\n allowOnDemandRefresh: boolean;\n /** Whether a store miss triggers a live Grafana read. */\n fetchOnDemand: boolean;\n /** Whether the panel routes (live dashboard/datasource queries) are served. */\n allowPanelQueries: boolean;\n /** Time-to-live for cached panel listings and panel data. */\n panelDataCacheTtl: HumanDuration;\n};\n\nconst DEFAULT_CACHE_TTL: HumanDuration = { minutes: 15 };\nconst DEFAULT_PANEL_DATA_CACHE_TTL: HumanDuration = { seconds: 30 };\n\n/**\n * Reads and validates the `grafana` section of the app configuration into a\n * fully-resolved {@link GrafanaBackendConfig}.\n *\n * @public\n */\nexport function readGrafanaConfig(rootConfig: Config): GrafanaBackendConfig {\n const instances = readGrafanaInstances(rootConfig);\n const config = rootConfig.getOptionalConfig('grafana');\n\n const store =\n (config?.getOptionalString('store') as GrafanaStoreKind | undefined) ??\n 'cache';\n if (store !== 'cache' && store !== 'database') {\n throw new InputError(\n `Invalid grafana.store '${store}', expected 'cache' or 'database'`,\n );\n }\n\n const scheduleConfig = config?.getOptionalConfig('schedule');\n\n return {\n instances,\n store,\n cacheTtl:\n (config?.getOptional('cacheTtl') as HumanDuration | undefined) ??\n DEFAULT_CACHE_TTL,\n schedule: scheduleConfig\n ? readSchedulerServiceTaskScheduleDefinitionFromConfig(scheduleConfig)\n : undefined,\n allowOnDemandRefresh:\n config?.getOptionalBoolean('allowOnDemandRefresh') ?? true,\n fetchOnDemand: config?.getOptionalBoolean('fetchOnDemand') ?? true,\n allowPanelQueries: config?.getOptionalBoolean('allowPanelQueries') ?? true,\n panelDataCacheTtl:\n (config?.getOptional('panelDataCacheTtl') as HumanDuration | undefined) ??\n DEFAULT_PANEL_DATA_CACHE_TTL,\n };\n}\n"],"names":["readGrafanaInstances","InputError","readSchedulerServiceTaskScheduleDefinitionFromConfig"],"mappings":";;;;;;AA4DA,MAAM,iBAAA,GAAmC,EAAE,OAAA,EAAS,EAAA,EAAG;AACvD,MAAM,4BAAA,GAA8C,EAAE,OAAA,EAAS,EAAA,EAAG;AAQ3D,SAAS,kBAAkB,UAAA,EAA0C;AAC1E,EAAA,MAAM,SAAA,GAAYA,gDAAqB,UAAU,CAAA;AACjD,EAAA,MAAM,MAAA,GAAS,UAAA,CAAW,iBAAA,CAAkB,SAAS,CAAA;AAErD,EAAA,MAAM,KAAA,GACH,MAAA,EAAQ,iBAAA,CAAkB,OAAO,CAAA,IAClC,OAAA;AACF,EAAA,IAAI,KAAA,KAAU,OAAA,IAAW,KAAA,KAAU,UAAA,EAAY;AAC7C,IAAA,MAAM,IAAIC,iBAAA;AAAA,MACR,0BAA0B,KAAK,CAAA,iCAAA;AAAA,KACjC;AAAA,EACF;AAEA,EAAA,MAAM,cAAA,GAAiB,MAAA,EAAQ,iBAAA,CAAkB,UAAU,CAAA;AAE3D,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA,EACG,MAAA,EAAQ,WAAA,CAAY,UAAU,CAAA,IAC/B,iBAAA;AAAA,IACF,QAAA,EAAU,cAAA,GACNC,qEAAA,CAAqD,cAAc,CAAA,GACnE,MAAA;AAAA,IACJ,oBAAA,EACE,MAAA,EAAQ,kBAAA,CAAmB,sBAAsB,CAAA,IAAK,IAAA;AAAA,IACxD,aAAA,EAAe,MAAA,EAAQ,kBAAA,CAAmB,eAAe,CAAA,IAAK,IAAA;AAAA,IAC9D,iBAAA,EAAmB,MAAA,EAAQ,kBAAA,CAAmB,mBAAmB,CAAA,IAAK,IAAA;AAAA,IACtE,iBAAA,EACG,MAAA,EAAQ,WAAA,CAAY,mBAAmB,CAAA,IACxC;AAAA,GACJ;AACF;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
|
-
import { LoggerService,
|
|
3
|
-
import {
|
|
2
|
+
import { LoggerService, CacheService, SchedulerServiceTaskScheduleDefinition, DatabaseService } from '@backstage/backend-plugin-api';
|
|
3
|
+
import { HumanDuration } from '@backstage/types';
|
|
4
|
+
import { GrafanaDashboard, GrafanaAlert, GrafanaInstanceInfo, GrafanaPanel, GrafanaPanelData } from '@marble-sh/backstage-plugin-grafana-common';
|
|
4
5
|
import { GrafanaInstanceConfig, GrafanaClient } from '@marble-sh/backstage-plugin-grafana-node';
|
|
5
6
|
export { FetchApi, GrafanaClient, GrafanaHttpClient, GrafanaInstanceApis, GrafanaInstanceConfig, ListAlertsOptions, ListDashboardsOptions } from '@marble-sh/backstage-plugin-grafana-node';
|
|
6
7
|
import { Config } from '@backstage/config';
|
|
7
|
-
import { HumanDuration } from '@backstage/types';
|
|
8
8
|
import { Knex } from 'knex';
|
|
9
9
|
import express from 'express';
|
|
10
10
|
|
|
@@ -65,6 +65,8 @@ type GetDashboardsOptions = {
|
|
|
65
65
|
tags?: string[];
|
|
66
66
|
/** Comma-separated title substrings; dashboards matching any are returned. */
|
|
67
67
|
query?: string;
|
|
68
|
+
/** Only return the dashboard with exactly this uid (case-sensitive). */
|
|
69
|
+
uid?: string;
|
|
68
70
|
/** Force a live fetch, bypassing the store. */
|
|
69
71
|
refresh?: boolean;
|
|
70
72
|
};
|
|
@@ -81,10 +83,41 @@ type GetAlertsOptions = {
|
|
|
81
83
|
/** Force a live fetch, bypassing the store. */
|
|
82
84
|
refresh?: boolean;
|
|
83
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* Options for listing the panels of a dashboard through the service.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
91
|
+
type GetPanelsOptions = {
|
|
92
|
+
/** The instance to read from. */
|
|
93
|
+
instanceName: string;
|
|
94
|
+
/** The uid of the dashboard whose panels are listed. */
|
|
95
|
+
dashboardUid: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Options for querying the data of a single panel through the service.
|
|
99
|
+
*
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
type GetPanelDataOptions = {
|
|
103
|
+
/** The instance to read from. */
|
|
104
|
+
instanceName: string;
|
|
105
|
+
/** The uid of the dashboard containing the panel. */
|
|
106
|
+
dashboardUid: string;
|
|
107
|
+
/** The id of the panel to query. */
|
|
108
|
+
panelId: number;
|
|
109
|
+
/** Range start: `now`, `now-<n><s|m|h|d|w>`, or epoch ms. Defaults to `now-6h`. */
|
|
110
|
+
from?: string;
|
|
111
|
+
/** Range end, same forms as `from`. Defaults to `now`. */
|
|
112
|
+
to?: string;
|
|
113
|
+
};
|
|
84
114
|
/**
|
|
85
115
|
* Reads dashboards and alerts from the configured Grafana instances, backed by a
|
|
86
116
|
* {@link GrafanaStore} for caching and periodic refresh.
|
|
87
117
|
*
|
|
118
|
+
* The panel methods are optional so that custom implementations that predate
|
|
119
|
+
* them stay valid; the router responds 404 when they are absent.
|
|
120
|
+
*
|
|
88
121
|
* @public
|
|
89
122
|
*/
|
|
90
123
|
interface GrafanaService {
|
|
@@ -96,6 +129,10 @@ interface GrafanaService {
|
|
|
96
129
|
getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]>;
|
|
97
130
|
/** Refreshes a single instance, or all instances when no name is given. */
|
|
98
131
|
refresh(instanceName?: string): Promise<void>;
|
|
132
|
+
/** Returns the panels of a single dashboard, live from Grafana. */
|
|
133
|
+
getPanels?(options: GetPanelsOptions): Promise<GrafanaPanel[]>;
|
|
134
|
+
/** Returns the queried data of a single panel, live from Grafana. */
|
|
135
|
+
getPanelData?(options: GetPanelDataOptions): Promise<GrafanaPanelData>;
|
|
99
136
|
}
|
|
100
137
|
/**
|
|
101
138
|
* The default {@link GrafanaService} implementation.
|
|
@@ -107,6 +144,8 @@ declare class DefaultGrafanaService implements GrafanaService {
|
|
|
107
144
|
private readonly store;
|
|
108
145
|
private readonly logger;
|
|
109
146
|
private readonly fetchOnDemand;
|
|
147
|
+
private readonly cache?;
|
|
148
|
+
private readonly panelCacheTtlMs;
|
|
110
149
|
constructor(options: {
|
|
111
150
|
instances: GrafanaInstance[];
|
|
112
151
|
store: GrafanaStore;
|
|
@@ -118,6 +157,14 @@ declare class DefaultGrafanaService implements GrafanaService {
|
|
|
118
157
|
* schedule, the refresh endpoints, or a `refresh: true` read option).
|
|
119
158
|
*/
|
|
120
159
|
fetchOnDemand?: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* When given, panel listings and panel data are cached here for
|
|
162
|
+
* `panelDataCacheTtl` to absorb bursts (a dashboard opening queries every
|
|
163
|
+
* panel at once). Without it, every panel request reads live.
|
|
164
|
+
*/
|
|
165
|
+
cache?: CacheService;
|
|
166
|
+
/** Time-to-live for cached panel data (default 30 seconds). */
|
|
167
|
+
panelDataCacheTtl?: HumanDuration;
|
|
121
168
|
});
|
|
122
169
|
/** {@inheritDoc GrafanaService.getInstances} */
|
|
123
170
|
getInstances(): GrafanaInstanceInfo[];
|
|
@@ -127,6 +174,11 @@ declare class DefaultGrafanaService implements GrafanaService {
|
|
|
127
174
|
getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]>;
|
|
128
175
|
/** {@inheritDoc GrafanaService.refresh} */
|
|
129
176
|
refresh(instanceName?: string): Promise<void>;
|
|
177
|
+
/** {@inheritDoc GrafanaService.getPanels} */
|
|
178
|
+
getPanels(options: GetPanelsOptions): Promise<GrafanaPanel[]>;
|
|
179
|
+
/** {@inheritDoc GrafanaService.getPanelData} */
|
|
180
|
+
getPanelData(options: GetPanelDataOptions): Promise<GrafanaPanelData>;
|
|
181
|
+
private withPanelCache;
|
|
130
182
|
private resolveNames;
|
|
131
183
|
private mustGet;
|
|
132
184
|
private snapshotFor;
|
|
@@ -153,6 +205,10 @@ type GrafanaBackendConfig = {
|
|
|
153
205
|
allowOnDemandRefresh: boolean;
|
|
154
206
|
/** Whether a store miss triggers a live Grafana read. */
|
|
155
207
|
fetchOnDemand: boolean;
|
|
208
|
+
/** Whether the panel routes (live dashboard/datasource queries) are served. */
|
|
209
|
+
allowPanelQueries: boolean;
|
|
210
|
+
/** Time-to-live for cached panel listings and panel data. */
|
|
211
|
+
panelDataCacheTtl: HumanDuration;
|
|
156
212
|
};
|
|
157
213
|
/**
|
|
158
214
|
* Reads and validates the `grafana` section of the app configuration into a
|
|
@@ -224,7 +280,13 @@ declare function createRouter(options: {
|
|
|
224
280
|
* routes respond 403.
|
|
225
281
|
*/
|
|
226
282
|
allowOnDemandRefresh?: boolean;
|
|
283
|
+
/**
|
|
284
|
+
* Whether the panel routes are served (default `true`). Panel listings and
|
|
285
|
+
* panel data always read live from Grafana, so `false` disables them (403)
|
|
286
|
+
* for deployments that want schedule-only Grafana traffic.
|
|
287
|
+
*/
|
|
288
|
+
allowPanelQueries?: boolean;
|
|
227
289
|
}): Promise<express.Router>;
|
|
228
290
|
|
|
229
291
|
export { CacheGrafanaStore, DatabaseGrafanaStore, DefaultGrafanaService, createRouter, grafanaPlugin as default, readGrafanaConfig };
|
|
230
|
-
export type { GetAlertsOptions, GetDashboardsOptions, GrafanaBackendConfig, GrafanaInstance, GrafanaService, GrafanaSnapshot, GrafanaStore, GrafanaStoreKind };
|
|
292
|
+
export type { GetAlertsOptions, GetDashboardsOptions, GetPanelDataOptions, GetPanelsOptions, GrafanaBackendConfig, GrafanaInstance, GrafanaService, GrafanaSnapshot, GrafanaStore, GrafanaStoreKind };
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -38,12 +38,15 @@ const grafanaPlugin = backendPluginApi.createBackendPlugin({
|
|
|
38
38
|
instances,
|
|
39
39
|
store,
|
|
40
40
|
logger,
|
|
41
|
-
fetchOnDemand: grafanaConfig.fetchOnDemand
|
|
41
|
+
fetchOnDemand: grafanaConfig.fetchOnDemand,
|
|
42
|
+
cache,
|
|
43
|
+
panelDataCacheTtl: grafanaConfig.panelDataCacheTtl
|
|
42
44
|
});
|
|
43
45
|
httpRouter.use(
|
|
44
46
|
await router.createRouter({
|
|
45
47
|
grafanaService,
|
|
46
|
-
allowOnDemandRefresh: grafanaConfig.allowOnDemandRefresh
|
|
48
|
+
allowOnDemandRefresh: grafanaConfig.allowOnDemandRefresh,
|
|
49
|
+
allowPanelQueries: grafanaConfig.allowPanelQueries
|
|
47
50
|
})
|
|
48
51
|
);
|
|
49
52
|
httpRouter.addAuthPolicy({ path: "/health", allow: "unauthenticated" });
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { readGrafanaConfig } from './grafana/config';\nimport { GrafanaHttpClient } from '@marble-sh/backstage-plugin-grafana-node';\nimport { CacheGrafanaStore } from './store/CacheGrafanaStore';\nimport { DatabaseGrafanaStore } from './store/DatabaseGrafanaStore';\nimport { GrafanaStore } from './store/GrafanaStore';\nimport {\n DefaultGrafanaService,\n GrafanaInstance,\n} from './service/GrafanaService';\nimport { createRouter } from './service/router';\n\n/**\n * The Grafana backend plugin.\n *\n * Reads dashboards and alerts from the configured Grafana instances, caches\n * them (in the cache or the database), optionally refreshes them on a schedule,\n * and exposes a read-only REST API under `/api/grafana`.\n *\n * @public\n */\nexport const grafanaPlugin = createBackendPlugin({\n pluginId: 'grafana',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n httpRouter: coreServices.httpRouter,\n cache: coreServices.cache,\n database: coreServices.database,\n scheduler: coreServices.scheduler,\n },\n async init({ logger, config, httpRouter, cache, database, scheduler }) {\n const grafanaConfig = readGrafanaConfig(config);\n\n if (grafanaConfig.instances.length === 0) {\n logger.warn(\n 'No Grafana instances are configured under `grafana.instances`; the grafana plugin will return empty results',\n );\n }\n\n const store: GrafanaStore =\n grafanaConfig.store === 'database'\n ? await DatabaseGrafanaStore.create({ database })\n : new CacheGrafanaStore({ cache, ttl: grafanaConfig.cacheTtl });\n\n const instances: GrafanaInstance[] = grafanaConfig.instances.map(\n instance => ({\n config: instance,\n client: new GrafanaHttpClient({ instance }),\n }),\n );\n\n const grafanaService = new DefaultGrafanaService({\n instances,\n store,\n logger,\n fetchOnDemand: grafanaConfig.fetchOnDemand,\n });\n\n httpRouter.use(\n await createRouter({\n grafanaService,\n allowOnDemandRefresh: grafanaConfig.allowOnDemandRefresh,\n }),\n );\n httpRouter.addAuthPolicy({ path: '/health', allow: 'unauthenticated' });\n\n if (grafanaConfig.schedule) {\n await scheduler.scheduleTask({\n id: 'grafana-refresh',\n ...grafanaConfig.schedule,\n fn: async () => {\n await grafanaService.refresh();\n },\n });\n logger.info('Scheduled periodic Grafana refresh');\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","config","readGrafanaConfig","DatabaseGrafanaStore","CacheGrafanaStore","GrafanaHttpClient","DefaultGrafanaService","createRouter"],"mappings":";;;;;;;;;;AAwCO,MAAM,gBAAgBA,oCAAA,CAAoB;AAAA,EAC/C,QAAA,EAAU,SAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,QAAQC,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,YAAYA,6BAAA,CAAa,UAAA;AAAA,QACzB,OAAOA,6BAAA,CAAa,KAAA;AAAA,QACpB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa;AAAA,OAC1B;AAAA,MACA,MAAM,KAAK,EAAE,MAAA,UAAQC,UAAQ,UAAA,EAAY,KAAA,EAAO,QAAA,EAAU,SAAA,EAAU,EAAG;AACrE,QAAA,MAAM,aAAA,GAAgBC,yBAAkBD,QAAM,CAAA;AAE9C,QAAA,IAAI,aAAA,CAAc,SAAA,CAAU,MAAA,KAAW,CAAA,EAAG;AACxC,UAAA,MAAA,CAAO,IAAA;AAAA,YACL;AAAA,WACF;AAAA,QACF;AAEA,QAAA,MAAM,QACJ,aAAA,CAAc,KAAA,KAAU,aACpB,MAAME,yCAAA,CAAqB,OAAO,EAAE,QAAA,EAAU,CAAA,GAC9C,IAAIC,mCAAA,CAAkB,EAAE,OAAO,GAAA,EAAK,aAAA,CAAc,UAAU,CAAA;AAElE,QAAA,MAAM,SAAA,GAA+B,cAAc,SAAA,CAAU,GAAA;AAAA,UAC3D,CAAA,QAAA,MAAa;AAAA,YACX,MAAA,EAAQ,QAAA;AAAA,YACR,MAAA,EAAQ,IAAIC,4CAAA,CAAkB,EAAE,UAAU;AAAA,WAC5C;AAAA,SACF;AAEA,QAAA,MAAM,cAAA,GAAiB,IAAIC,oCAAA,CAAsB;AAAA,UAC/C,SAAA;AAAA,UACA,KAAA;AAAA,UACA,MAAA;AAAA,UACA,eAAe,aAAA,CAAc;AAAA,
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { readGrafanaConfig } from './grafana/config';\nimport { GrafanaHttpClient } from '@marble-sh/backstage-plugin-grafana-node';\nimport { CacheGrafanaStore } from './store/CacheGrafanaStore';\nimport { DatabaseGrafanaStore } from './store/DatabaseGrafanaStore';\nimport { GrafanaStore } from './store/GrafanaStore';\nimport {\n DefaultGrafanaService,\n GrafanaInstance,\n} from './service/GrafanaService';\nimport { createRouter } from './service/router';\n\n/**\n * The Grafana backend plugin.\n *\n * Reads dashboards and alerts from the configured Grafana instances, caches\n * them (in the cache or the database), optionally refreshes them on a schedule,\n * and exposes a read-only REST API under `/api/grafana`.\n *\n * @public\n */\nexport const grafanaPlugin = createBackendPlugin({\n pluginId: 'grafana',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n httpRouter: coreServices.httpRouter,\n cache: coreServices.cache,\n database: coreServices.database,\n scheduler: coreServices.scheduler,\n },\n async init({ logger, config, httpRouter, cache, database, scheduler }) {\n const grafanaConfig = readGrafanaConfig(config);\n\n if (grafanaConfig.instances.length === 0) {\n logger.warn(\n 'No Grafana instances are configured under `grafana.instances`; the grafana plugin will return empty results',\n );\n }\n\n const store: GrafanaStore =\n grafanaConfig.store === 'database'\n ? await DatabaseGrafanaStore.create({ database })\n : new CacheGrafanaStore({ cache, ttl: grafanaConfig.cacheTtl });\n\n const instances: GrafanaInstance[] = grafanaConfig.instances.map(\n instance => ({\n config: instance,\n client: new GrafanaHttpClient({ instance }),\n }),\n );\n\n const grafanaService = new DefaultGrafanaService({\n instances,\n store,\n logger,\n fetchOnDemand: grafanaConfig.fetchOnDemand,\n cache,\n panelDataCacheTtl: grafanaConfig.panelDataCacheTtl,\n });\n\n httpRouter.use(\n await createRouter({\n grafanaService,\n allowOnDemandRefresh: grafanaConfig.allowOnDemandRefresh,\n allowPanelQueries: grafanaConfig.allowPanelQueries,\n }),\n );\n httpRouter.addAuthPolicy({ path: '/health', allow: 'unauthenticated' });\n\n if (grafanaConfig.schedule) {\n await scheduler.scheduleTask({\n id: 'grafana-refresh',\n ...grafanaConfig.schedule,\n fn: async () => {\n await grafanaService.refresh();\n },\n });\n logger.info('Scheduled periodic Grafana refresh');\n }\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","config","readGrafanaConfig","DatabaseGrafanaStore","CacheGrafanaStore","GrafanaHttpClient","DefaultGrafanaService","createRouter"],"mappings":";;;;;;;;;;AAwCO,MAAM,gBAAgBA,oCAAA,CAAoB;AAAA,EAC/C,QAAA,EAAU,SAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,QAAQC,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa,UAAA;AAAA,QACrB,YAAYA,6BAAA,CAAa,UAAA;AAAA,QACzB,OAAOA,6BAAA,CAAa,KAAA;AAAA,QACpB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,WAAWA,6BAAA,CAAa;AAAA,OAC1B;AAAA,MACA,MAAM,KAAK,EAAE,MAAA,UAAQC,UAAQ,UAAA,EAAY,KAAA,EAAO,QAAA,EAAU,SAAA,EAAU,EAAG;AACrE,QAAA,MAAM,aAAA,GAAgBC,yBAAkBD,QAAM,CAAA;AAE9C,QAAA,IAAI,aAAA,CAAc,SAAA,CAAU,MAAA,KAAW,CAAA,EAAG;AACxC,UAAA,MAAA,CAAO,IAAA;AAAA,YACL;AAAA,WACF;AAAA,QACF;AAEA,QAAA,MAAM,QACJ,aAAA,CAAc,KAAA,KAAU,aACpB,MAAME,yCAAA,CAAqB,OAAO,EAAE,QAAA,EAAU,CAAA,GAC9C,IAAIC,mCAAA,CAAkB,EAAE,OAAO,GAAA,EAAK,aAAA,CAAc,UAAU,CAAA;AAElE,QAAA,MAAM,SAAA,GAA+B,cAAc,SAAA,CAAU,GAAA;AAAA,UAC3D,CAAA,QAAA,MAAa;AAAA,YACX,MAAA,EAAQ,QAAA;AAAA,YACR,MAAA,EAAQ,IAAIC,4CAAA,CAAkB,EAAE,UAAU;AAAA,WAC5C;AAAA,SACF;AAEA,QAAA,MAAM,cAAA,GAAiB,IAAIC,oCAAA,CAAsB;AAAA,UAC/C,SAAA;AAAA,UACA,KAAA;AAAA,UACA,MAAA;AAAA,UACA,eAAe,aAAA,CAAc,aAAA;AAAA,UAC7B,KAAA;AAAA,UACA,mBAAmB,aAAA,CAAc;AAAA,SAClC,CAAA;AAED,QAAA,UAAA,CAAW,GAAA;AAAA,UACT,MAAMC,mBAAA,CAAa;AAAA,YACjB,cAAA;AAAA,YACA,sBAAsB,aAAA,CAAc,oBAAA;AAAA,YACpC,mBAAmB,aAAA,CAAc;AAAA,WAClC;AAAA,SACH;AACA,QAAA,UAAA,CAAW,cAAc,EAAE,IAAA,EAAM,SAAA,EAAW,KAAA,EAAO,mBAAmB,CAAA;AAEtE,QAAA,IAAI,cAAc,QAAA,EAAU;AAC1B,UAAA,MAAM,UAAU,YAAA,CAAa;AAAA,YAC3B,EAAA,EAAI,iBAAA;AAAA,YACJ,GAAG,aAAA,CAAc,QAAA;AAAA,YACjB,IAAI,YAAY;AACd,cAAA,MAAM,eAAe,OAAA,EAAQ;AAAA,YAC/B;AAAA,WACD,CAAA;AACD,UAAA,MAAA,CAAO,KAAK,oCAAoC,CAAA;AAAA,QAClD;AAAA,MACF;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var errors = require('@backstage/errors');
|
|
4
|
+
var types = require('@backstage/types');
|
|
4
5
|
var backstagePluginGrafanaNode = require('@marble-sh/backstage-plugin-grafana-node');
|
|
5
6
|
|
|
6
7
|
class DefaultGrafanaService {
|
|
@@ -8,6 +9,8 @@ class DefaultGrafanaService {
|
|
|
8
9
|
store;
|
|
9
10
|
logger;
|
|
10
11
|
fetchOnDemand;
|
|
12
|
+
cache;
|
|
13
|
+
panelCacheTtlMs;
|
|
11
14
|
constructor(options) {
|
|
12
15
|
this.instances = new Map(
|
|
13
16
|
options.instances.map((instance) => [instance.config.name, instance])
|
|
@@ -15,6 +18,10 @@ class DefaultGrafanaService {
|
|
|
15
18
|
this.store = options.store;
|
|
16
19
|
this.logger = options.logger;
|
|
17
20
|
this.fetchOnDemand = options.fetchOnDemand ?? true;
|
|
21
|
+
this.cache = options.cache;
|
|
22
|
+
this.panelCacheTtlMs = types.durationToMilliseconds(
|
|
23
|
+
options.panelDataCacheTtl ?? { seconds: 30 }
|
|
24
|
+
);
|
|
18
25
|
}
|
|
19
26
|
/** {@inheritDoc GrafanaService.getInstances} */
|
|
20
27
|
getInstances() {
|
|
@@ -33,7 +40,8 @@ class DefaultGrafanaService {
|
|
|
33
40
|
result.push(
|
|
34
41
|
...backstagePluginGrafanaNode.filterDashboards(snapshot.dashboards, {
|
|
35
42
|
tags: options.tags,
|
|
36
|
-
query: options.query
|
|
43
|
+
query: options.query,
|
|
44
|
+
uid: options.uid
|
|
37
45
|
})
|
|
38
46
|
);
|
|
39
47
|
}
|
|
@@ -70,6 +78,49 @@ class DefaultGrafanaService {
|
|
|
70
78
|
}
|
|
71
79
|
}
|
|
72
80
|
}
|
|
81
|
+
/** {@inheritDoc GrafanaService.getPanels} */
|
|
82
|
+
async getPanels(options) {
|
|
83
|
+
const { client } = this.mustGet(options.instanceName);
|
|
84
|
+
if (!client.getPanels) {
|
|
85
|
+
throw new errors.NotFoundError(
|
|
86
|
+
`The Grafana client for instance '${options.instanceName}' does not support panel queries`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return this.withPanelCache(
|
|
90
|
+
`panels:v1:${options.instanceName}:${options.dashboardUid}`,
|
|
91
|
+
() => client.getPanels(options.dashboardUid)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
/** {@inheritDoc GrafanaService.getPanelData} */
|
|
95
|
+
async getPanelData(options) {
|
|
96
|
+
const { client } = this.mustGet(options.instanceName);
|
|
97
|
+
if (!client.getPanelData) {
|
|
98
|
+
throw new errors.NotFoundError(
|
|
99
|
+
`The Grafana client for instance '${options.instanceName}' does not support panel queries`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const from = options.from ?? "now-6h";
|
|
103
|
+
const to = options.to ?? "now";
|
|
104
|
+
return this.withPanelCache(
|
|
105
|
+
`panel-data:v1:${options.instanceName}:${options.dashboardUid}:${options.panelId}:${from}:${to}`,
|
|
106
|
+
() => client.getPanelData(options.dashboardUid, options.panelId, {
|
|
107
|
+
from,
|
|
108
|
+
to
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
async withPanelCache(key, fn) {
|
|
113
|
+
if (!this.cache) {
|
|
114
|
+
return fn();
|
|
115
|
+
}
|
|
116
|
+
const cached = await this.cache.get(key);
|
|
117
|
+
if (cached !== void 0) {
|
|
118
|
+
return cached;
|
|
119
|
+
}
|
|
120
|
+
const value = await fn();
|
|
121
|
+
await this.cache.set(key, value, { ttl: this.panelCacheTtlMs });
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
73
124
|
resolveNames(instanceName) {
|
|
74
125
|
if (instanceName) {
|
|
75
126
|
this.mustGet(instanceName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GrafanaService.cjs.js","sources":["../../src/service/GrafanaService.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { LoggerService } from '@backstage/backend-plugin-api';\nimport { NotFoundError } from '@backstage/errors';\nimport {\n GrafanaAlert,\n GrafanaDashboard,\n GrafanaInstanceInfo,\n} from '@marble-sh/backstage-plugin-grafana-common';\nimport {\n filterAlerts,\n filterDashboards,\n GrafanaClient,\n GrafanaInstanceConfig,\n} from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaSnapshot, GrafanaStore } from '../store/GrafanaStore';\n\n/**\n * A configured instance together with the client used to read from it.\n *\n * @public\n */\nexport type GrafanaInstance = {\n config: GrafanaInstanceConfig;\n client: GrafanaClient;\n};\n\n/**\n * Options for reading dashboards through the service.\n *\n * @public\n */\nexport type GetDashboardsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return dashboards carrying all of these tags. */\n tags?: string[];\n /** Comma-separated title substrings; dashboards matching any are returned. */\n query?: string;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Options for reading alerts through the service.\n *\n * @public\n */\nexport type GetAlertsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return alerts whose labels match all of these `key=value` pairs. */\n labelSelector?: Record<string, string>;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Reads dashboards and alerts from the configured Grafana instances, backed by a\n * {@link GrafanaStore} for caching and periodic refresh.\n *\n * @public\n */\nexport interface GrafanaService {\n /** Returns the configured Grafana instances. */\n getInstances(): GrafanaInstanceInfo[];\n /** Returns dashboards for one or all instances, honoring the store and filters. */\n getDashboards(options: GetDashboardsOptions): Promise<GrafanaDashboard[]>;\n /** Returns alerts for one or all instances, honoring the store and filters. */\n getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]>;\n /** Refreshes a single instance, or all instances when no name is given. */\n refresh(instanceName?: string): Promise<void>;\n}\n\n/**\n * The default {@link GrafanaService} implementation.\n *\n * @public\n */\nexport class DefaultGrafanaService implements GrafanaService {\n private readonly instances: Map<string, GrafanaInstance>;\n private readonly store: GrafanaStore;\n private readonly logger: LoggerService;\n private readonly fetchOnDemand: boolean;\n\n constructor(options: {\n instances: GrafanaInstance[];\n store: GrafanaStore;\n logger: LoggerService;\n /**\n * Whether a store miss triggers a live Grafana read (default `true`).\n * When `false`, misses resolve to an empty snapshot and Grafana is only\n * contacted by explicit {@link DefaultGrafanaService.refresh} calls (the\n * schedule, the refresh endpoints, or a `refresh: true` read option).\n */\n fetchOnDemand?: boolean;\n }) {\n this.instances = new Map(\n options.instances.map(instance => [instance.config.name, instance]),\n );\n this.store = options.store;\n this.logger = options.logger;\n this.fetchOnDemand = options.fetchOnDemand ?? true;\n }\n\n /** {@inheritDoc GrafanaService.getInstances} */\n getInstances(): GrafanaInstanceInfo[] {\n return [...this.instances.values()].map(({ config }) => ({\n name: config.name,\n title: config.title,\n url: config.baseUrl,\n }));\n }\n\n /** {@inheritDoc GrafanaService.getDashboards} */\n async getDashboards(\n options: GetDashboardsOptions,\n ): Promise<GrafanaDashboard[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaDashboard[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterDashboards(snapshot.dashboards, {\n tags: options.tags,\n query: options.query,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.getAlerts} */\n async getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaAlert[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterAlerts(snapshot.alerts, {\n labelSelector: options.labelSelector,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.refresh} */\n async refresh(instanceName?: string): Promise<void> {\n if (instanceName) {\n await this.refreshInstance(instanceName);\n return;\n }\n for (const name of this.instances.keys()) {\n try {\n await this.refreshInstance(name);\n } catch (error) {\n this.logger.warn(\n `Failed to refresh Grafana instance '${name}'`,\n error as Error,\n );\n }\n }\n }\n\n private resolveNames(instanceName?: string): string[] {\n if (instanceName) {\n this.mustGet(instanceName);\n return [instanceName];\n }\n return [...this.instances.keys()];\n }\n\n private mustGet(instanceName: string): GrafanaInstance {\n const instance = this.instances.get(instanceName);\n if (!instance) {\n throw new NotFoundError(\n `No Grafana instance configured with name '${instanceName}'`,\n );\n }\n return instance;\n }\n\n private async snapshotFor(\n instanceName: string,\n refresh?: boolean,\n ): Promise<Pick<GrafanaSnapshot, 'dashboards' | 'alerts'>> {\n if (!refresh) {\n const cached = await this.store.get(instanceName);\n if (cached) {\n return cached;\n }\n if (!this.fetchOnDemand) {\n // Serve the miss as empty rather than reaching for Grafana; nothing\n // is stored, so results fill in as soon as a refresh runs.\n return { dashboards: [], alerts: [] };\n }\n }\n return this.refreshInstance(instanceName);\n }\n\n private async refreshInstance(\n instanceName: string,\n ): Promise<GrafanaSnapshot> {\n const { client } = this.mustGet(instanceName);\n const [dashboards, alerts] = await Promise.all([\n client.listDashboards(),\n client.listAlerts(),\n ]);\n const snapshot: GrafanaSnapshot = {\n dashboards,\n alerts,\n fetchedAt: new Date().toISOString(),\n };\n await this.store.set(instanceName, snapshot);\n this.logger.debug(\n `Refreshed Grafana instance '${instanceName}': ${dashboards.length} dashboards, ${alerts.length} alerts`,\n );\n return snapshot;\n }\n}\n"],"names":["filterDashboards","filterAlerts","NotFoundError"],"mappings":";;;;;AA6FO,MAAM,qBAAA,CAAgD;AAAA,EAC1C,SAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,aAAA;AAAA,EAEjB,YAAY,OAAA,EAWT;AACD,IAAA,IAAA,CAAK,YAAY,IAAI,GAAA;AAAA,MACnB,OAAA,CAAQ,UAAU,GAAA,CAAI,CAAA,QAAA,KAAY,CAAC,QAAA,CAAS,MAAA,CAAO,IAAA,EAAM,QAAQ,CAAC;AAAA,KACpE;AACA,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,KAAA;AACrB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,aAAA,GAAgB,QAAQ,aAAA,IAAiB,IAAA;AAAA,EAChD;AAAA;AAAA,EAGA,YAAA,GAAsC;AACpC,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,SAAA,CAAU,MAAA,EAAQ,CAAA,CAAE,GAAA,CAAI,CAAC,EAAE,MAAA,EAAO,MAAO;AAAA,MACvD,MAAM,MAAA,CAAO,IAAA;AAAA,MACb,OAAO,MAAA,CAAO,KAAA;AAAA,MACd,KAAK,MAAA,CAAO;AAAA,KACd,CAAE,CAAA;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,cACJ,OAAA,EAC6B;AAC7B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,YAAY,CAAA;AACpD,IAAA,MAAM,SAA6B,EAAC;AACpC,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,MAAA,MAAM,WAAW,MAAM,IAAA,CAAK,WAAA,CAAY,IAAA,EAAM,QAAQ,OAAO,CAAA;AAC7D,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,GAAGA,2CAAA,CAAiB,QAAA,CAAS,UAAA,EAAY;AAAA,UACvC,MAAM,OAAA,CAAQ,IAAA;AAAA,UACd,OAAO,OAAA,CAAQ;AAAA,SAChB;AAAA,OACH;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,UAAU,OAAA,EAAoD;AAClE,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,YAAY,CAAA;AACpD,IAAA,MAAM,SAAyB,EAAC;AAChC,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,MAAA,MAAM,WAAW,MAAM,IAAA,CAAK,WAAA,CAAY,IAAA,EAAM,QAAQ,OAAO,CAAA;AAC7D,MAAA,MAAA,CAAO,IAAA;AAAA,QACL,GAAGC,uCAAA,CAAa,QAAA,CAAS,MAAA,EAAQ;AAAA,UAC/B,eAAe,OAAA,CAAQ;AAAA,SACxB;AAAA,OACH;AAAA,IACF;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,QAAQ,YAAA,EAAsC;AAClD,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,MAAM,IAAA,CAAK,gBAAgB,YAAY,CAAA;AACvC,MAAA;AAAA,IACF;AACA,IAAA,KAAA,MAAW,IAAA,IAAQ,IAAA,CAAK,SAAA,CAAU,IAAA,EAAK,EAAG;AACxC,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,gBAAgB,IAAI,CAAA;AAAA,MACjC,SAAS,KAAA,EAAO;AACd,QAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,UACV,uCAAuC,IAAI,CAAA,CAAA,CAAA;AAAA,UAC3C;AAAA,SACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,aAAa,YAAA,EAAiC;AACpD,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,IAAA,CAAK,QAAQ,YAAY,CAAA;AACzB,MAAA,OAAO,CAAC,YAAY,CAAA;AAAA,IACtB;AACA,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,SAAA,CAAU,MAAM,CAAA;AAAA,EAClC;AAAA,EAEQ,QAAQ,YAAA,EAAuC;AACrD,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,YAAY,CAAA;AAChD,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,MAAM,IAAIC,oBAAA;AAAA,QACR,6CAA6C,YAAY,CAAA,CAAA;AAAA,OAC3D;AAAA,IACF;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA,EAEA,MAAc,WAAA,CACZ,YAAA,EACA,OAAA,EACyD;AACzD,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA,CAAM,IAAI,YAAY,CAAA;AAChD,MAAA,IAAI,MAAA,EAAQ;AACV,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,IAAI,CAAC,KAAK,aAAA,EAAe;AAGvB,QAAA,OAAO,EAAE,UAAA,EAAY,EAAC,EAAG,MAAA,EAAQ,EAAC,EAAE;AAAA,MACtC;AAAA,IACF;AACA,IAAA,OAAO,IAAA,CAAK,gBAAgB,YAAY,CAAA;AAAA,EAC1C;AAAA,EAEA,MAAc,gBACZ,YAAA,EAC0B;AAC1B,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,IAAA,CAAK,QAAQ,YAAY,CAAA;AAC5C,IAAA,MAAM,CAAC,UAAA,EAAY,MAAM,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,MAC7C,OAAO,cAAA,EAAe;AAAA,MACtB,OAAO,UAAA;AAAW,KACnB,CAAA;AACD,IAAA,MAAM,QAAA,GAA4B;AAAA,MAChC,UAAA;AAAA,MACA,MAAA;AAAA,MACA,SAAA,EAAA,iBAAW,IAAI,IAAA,EAAK,EAAE,WAAA;AAAY,KACpC;AACA,IAAA,MAAM,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,YAAA,EAAc,QAAQ,CAAA;AAC3C,IAAA,IAAA,CAAK,MAAA,CAAO,KAAA;AAAA,MACV,+BAA+B,YAAY,CAAA,GAAA,EAAM,WAAW,MAAM,CAAA,aAAA,EAAgB,OAAO,MAAM,CAAA,OAAA;AAAA,KACjG;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AACF;;"}
|
|
1
|
+
{"version":3,"file":"GrafanaService.cjs.js","sources":["../../src/service/GrafanaService.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { CacheService, LoggerService } from '@backstage/backend-plugin-api';\nimport { NotFoundError } from '@backstage/errors';\nimport { HumanDuration, durationToMilliseconds } from '@backstage/types';\nimport {\n GrafanaAlert,\n GrafanaDashboard,\n GrafanaInstanceInfo,\n GrafanaPanel,\n GrafanaPanelData,\n} from '@marble-sh/backstage-plugin-grafana-common';\nimport {\n filterAlerts,\n filterDashboards,\n GrafanaClient,\n GrafanaInstanceConfig,\n} from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaSnapshot, GrafanaStore } from '../store/GrafanaStore';\n\n/**\n * A configured instance together with the client used to read from it.\n *\n * @public\n */\nexport type GrafanaInstance = {\n config: GrafanaInstanceConfig;\n client: GrafanaClient;\n};\n\n/**\n * Options for reading dashboards through the service.\n *\n * @public\n */\nexport type GetDashboardsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return dashboards carrying all of these tags. */\n tags?: string[];\n /** Comma-separated title substrings; dashboards matching any are returned. */\n query?: string;\n /** Only return the dashboard with exactly this uid (case-sensitive). */\n uid?: string;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Options for reading alerts through the service.\n *\n * @public\n */\nexport type GetAlertsOptions = {\n /** Restrict to a single instance. When omitted, all instances are queried. */\n instanceName?: string;\n /** Only return alerts whose labels match all of these `key=value` pairs. */\n labelSelector?: Record<string, string>;\n /** Force a live fetch, bypassing the store. */\n refresh?: boolean;\n};\n\n/**\n * Options for listing the panels of a dashboard through the service.\n *\n * @public\n */\nexport type GetPanelsOptions = {\n /** The instance to read from. */\n instanceName: string;\n /** The uid of the dashboard whose panels are listed. */\n dashboardUid: string;\n};\n\n/**\n * Options for querying the data of a single panel through the service.\n *\n * @public\n */\nexport type GetPanelDataOptions = {\n /** The instance to read from. */\n instanceName: string;\n /** The uid of the dashboard containing the panel. */\n dashboardUid: string;\n /** The id of the panel to query. */\n panelId: number;\n /** Range start: `now`, `now-<n><s|m|h|d|w>`, or epoch ms. Defaults to `now-6h`. */\n from?: string;\n /** Range end, same forms as `from`. Defaults to `now`. */\n to?: string;\n};\n\n/**\n * Reads dashboards and alerts from the configured Grafana instances, backed by a\n * {@link GrafanaStore} for caching and periodic refresh.\n *\n * The panel methods are optional so that custom implementations that predate\n * them stay valid; the router responds 404 when they are absent.\n *\n * @public\n */\nexport interface GrafanaService {\n /** Returns the configured Grafana instances. */\n getInstances(): GrafanaInstanceInfo[];\n /** Returns dashboards for one or all instances, honoring the store and filters. */\n getDashboards(options: GetDashboardsOptions): Promise<GrafanaDashboard[]>;\n /** Returns alerts for one or all instances, honoring the store and filters. */\n getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]>;\n /** Refreshes a single instance, or all instances when no name is given. */\n refresh(instanceName?: string): Promise<void>;\n /** Returns the panels of a single dashboard, live from Grafana. */\n getPanels?(options: GetPanelsOptions): Promise<GrafanaPanel[]>;\n /** Returns the queried data of a single panel, live from Grafana. */\n getPanelData?(options: GetPanelDataOptions): Promise<GrafanaPanelData>;\n}\n\n/**\n * The default {@link GrafanaService} implementation.\n *\n * @public\n */\nexport class DefaultGrafanaService implements GrafanaService {\n private readonly instances: Map<string, GrafanaInstance>;\n private readonly store: GrafanaStore;\n private readonly logger: LoggerService;\n private readonly fetchOnDemand: boolean;\n private readonly cache?: CacheService;\n private readonly panelCacheTtlMs: number;\n\n constructor(options: {\n instances: GrafanaInstance[];\n store: GrafanaStore;\n logger: LoggerService;\n /**\n * Whether a store miss triggers a live Grafana read (default `true`).\n * When `false`, misses resolve to an empty snapshot and Grafana is only\n * contacted by explicit {@link DefaultGrafanaService.refresh} calls (the\n * schedule, the refresh endpoints, or a `refresh: true` read option).\n */\n fetchOnDemand?: boolean;\n /**\n * When given, panel listings and panel data are cached here for\n * `panelDataCacheTtl` to absorb bursts (a dashboard opening queries every\n * panel at once). Without it, every panel request reads live.\n */\n cache?: CacheService;\n /** Time-to-live for cached panel data (default 30 seconds). */\n panelDataCacheTtl?: HumanDuration;\n }) {\n this.instances = new Map(\n options.instances.map(instance => [instance.config.name, instance]),\n );\n this.store = options.store;\n this.logger = options.logger;\n this.fetchOnDemand = options.fetchOnDemand ?? true;\n this.cache = options.cache;\n this.panelCacheTtlMs = durationToMilliseconds(\n options.panelDataCacheTtl ?? { seconds: 30 },\n );\n }\n\n /** {@inheritDoc GrafanaService.getInstances} */\n getInstances(): GrafanaInstanceInfo[] {\n return [...this.instances.values()].map(({ config }) => ({\n name: config.name,\n title: config.title,\n url: config.baseUrl,\n }));\n }\n\n /** {@inheritDoc GrafanaService.getDashboards} */\n async getDashboards(\n options: GetDashboardsOptions,\n ): Promise<GrafanaDashboard[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaDashboard[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterDashboards(snapshot.dashboards, {\n tags: options.tags,\n query: options.query,\n uid: options.uid,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.getAlerts} */\n async getAlerts(options: GetAlertsOptions): Promise<GrafanaAlert[]> {\n const names = this.resolveNames(options.instanceName);\n const result: GrafanaAlert[] = [];\n for (const name of names) {\n const snapshot = await this.snapshotFor(name, options.refresh);\n result.push(\n ...filterAlerts(snapshot.alerts, {\n labelSelector: options.labelSelector,\n }),\n );\n }\n return result;\n }\n\n /** {@inheritDoc GrafanaService.refresh} */\n async refresh(instanceName?: string): Promise<void> {\n if (instanceName) {\n await this.refreshInstance(instanceName);\n return;\n }\n for (const name of this.instances.keys()) {\n try {\n await this.refreshInstance(name);\n } catch (error) {\n this.logger.warn(\n `Failed to refresh Grafana instance '${name}'`,\n error as Error,\n );\n }\n }\n }\n\n /** {@inheritDoc GrafanaService.getPanels} */\n async getPanels(options: GetPanelsOptions): Promise<GrafanaPanel[]> {\n const { client } = this.mustGet(options.instanceName);\n if (!client.getPanels) {\n throw new NotFoundError(\n `The Grafana client for instance '${options.instanceName}' does not support panel queries`,\n );\n }\n return this.withPanelCache(\n `panels:v1:${options.instanceName}:${options.dashboardUid}`,\n () => client.getPanels!(options.dashboardUid),\n );\n }\n\n /** {@inheritDoc GrafanaService.getPanelData} */\n async getPanelData(options: GetPanelDataOptions): Promise<GrafanaPanelData> {\n const { client } = this.mustGet(options.instanceName);\n if (!client.getPanelData) {\n throw new NotFoundError(\n `The Grafana client for instance '${options.instanceName}' does not support panel queries`,\n );\n }\n const from = options.from ?? 'now-6h';\n const to = options.to ?? 'now';\n return this.withPanelCache(\n `panel-data:v1:${options.instanceName}:${options.dashboardUid}:${options.panelId}:${from}:${to}`,\n () =>\n client.getPanelData!(options.dashboardUid, options.panelId, {\n from,\n to,\n }),\n );\n }\n\n private async withPanelCache<T>(\n key: string,\n fn: () => Promise<T>,\n ): Promise<T> {\n if (!this.cache) {\n return fn();\n }\n const cached = await this.cache.get(key);\n if (cached !== undefined) {\n return cached as T;\n }\n const value = await fn();\n await this.cache.set(key, value as never, { ttl: this.panelCacheTtlMs });\n return value;\n }\n\n private resolveNames(instanceName?: string): string[] {\n if (instanceName) {\n this.mustGet(instanceName);\n return [instanceName];\n }\n return [...this.instances.keys()];\n }\n\n private mustGet(instanceName: string): GrafanaInstance {\n const instance = this.instances.get(instanceName);\n if (!instance) {\n throw new NotFoundError(\n `No Grafana instance configured with name '${instanceName}'`,\n );\n }\n return instance;\n }\n\n private async snapshotFor(\n instanceName: string,\n refresh?: boolean,\n ): Promise<Pick<GrafanaSnapshot, 'dashboards' | 'alerts'>> {\n if (!refresh) {\n const cached = await this.store.get(instanceName);\n if (cached) {\n return cached;\n }\n if (!this.fetchOnDemand) {\n // Serve the miss as empty rather than reaching for Grafana; nothing\n // is stored, so results fill in as soon as a refresh runs.\n return { dashboards: [], alerts: [] };\n }\n }\n return this.refreshInstance(instanceName);\n }\n\n private async refreshInstance(\n instanceName: string,\n ): Promise<GrafanaSnapshot> {\n const { client } = this.mustGet(instanceName);\n const [dashboards, alerts] = await Promise.all([\n client.listDashboards(),\n client.listAlerts(),\n ]);\n const snapshot: GrafanaSnapshot = {\n dashboards,\n alerts,\n fetchedAt: new Date().toISOString(),\n };\n await this.store.set(instanceName, snapshot);\n this.logger.debug(\n `Refreshed Grafana instance '${instanceName}': ${dashboards.length} dashboards, ${alerts.length} alerts`,\n );\n return snapshot;\n }\n}\n"],"names":["durationToMilliseconds","filterDashboards","filterAlerts","NotFoundError"],"mappings":"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"}
|
|
@@ -21,6 +21,7 @@ const toBoolean = (value) => value !== void 0 && ["true", "1", ""].includes(Stri
|
|
|
21
21
|
async function createRouter(options) {
|
|
22
22
|
const { grafanaService } = options;
|
|
23
23
|
const allowOnDemandRefresh = options.allowOnDemandRefresh ?? true;
|
|
24
|
+
const allowPanelQueries = options.allowPanelQueries ?? true;
|
|
24
25
|
const router = Router__default.default();
|
|
25
26
|
router.use(express__default.default.json());
|
|
26
27
|
const toRefresh = (value) => allowOnDemandRefresh && toBoolean(value);
|
|
@@ -31,6 +32,13 @@ async function createRouter(options) {
|
|
|
31
32
|
);
|
|
32
33
|
}
|
|
33
34
|
};
|
|
35
|
+
const assertPanelQueriesAllowed = () => {
|
|
36
|
+
if (!allowPanelQueries) {
|
|
37
|
+
throw new errors.NotAllowedError(
|
|
38
|
+
"Panel queries are disabled by configuration (grafana.allowPanelQueries)"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
34
42
|
router.get("/health", (_req, res) => {
|
|
35
43
|
res.json({ status: "ok" });
|
|
36
44
|
});
|
|
@@ -42,6 +50,7 @@ async function createRouter(options) {
|
|
|
42
50
|
instanceName: req.params.name,
|
|
43
51
|
tags: toArray(req.query.tag),
|
|
44
52
|
query: toString(req.query.query),
|
|
53
|
+
uid: toString(req.query.uid),
|
|
45
54
|
refresh: toRefresh(req.query.refresh)
|
|
46
55
|
});
|
|
47
56
|
res.json({ items });
|
|
@@ -54,6 +63,43 @@ async function createRouter(options) {
|
|
|
54
63
|
});
|
|
55
64
|
res.json({ items });
|
|
56
65
|
});
|
|
66
|
+
router.get("/instances/:name/dashboards/:uid/panels", async (req, res) => {
|
|
67
|
+
assertPanelQueriesAllowed();
|
|
68
|
+
if (!grafanaService.getPanels) {
|
|
69
|
+
throw new errors.NotFoundError(
|
|
70
|
+
"Panel queries are not supported by the configured Grafana service"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const items = await grafanaService.getPanels({
|
|
74
|
+
instanceName: req.params.name,
|
|
75
|
+
dashboardUid: req.params.uid
|
|
76
|
+
});
|
|
77
|
+
res.json({ items });
|
|
78
|
+
});
|
|
79
|
+
router.get(
|
|
80
|
+
"/instances/:name/dashboards/:uid/panels/:panelId/data",
|
|
81
|
+
async (req, res) => {
|
|
82
|
+
assertPanelQueriesAllowed();
|
|
83
|
+
if (!grafanaService.getPanelData) {
|
|
84
|
+
throw new errors.NotFoundError(
|
|
85
|
+
"Panel queries are not supported by the configured Grafana service"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!/^\d+$/.test(req.params.panelId)) {
|
|
89
|
+
throw new errors.InputError(
|
|
90
|
+
`Invalid panel id '${req.params.panelId}', expected an integer`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const data = await grafanaService.getPanelData({
|
|
94
|
+
instanceName: req.params.name,
|
|
95
|
+
dashboardUid: req.params.uid,
|
|
96
|
+
panelId: Number(req.params.panelId),
|
|
97
|
+
from: toString(req.query.from),
|
|
98
|
+
to: toString(req.query.to)
|
|
99
|
+
});
|
|
100
|
+
res.json(data);
|
|
101
|
+
}
|
|
102
|
+
);
|
|
57
103
|
router.post("/instances/:name/refresh", async (req, res) => {
|
|
58
104
|
assertRefreshAllowed();
|
|
59
105
|
await grafanaService.refresh(req.params.name);
|
|
@@ -64,6 +110,7 @@ async function createRouter(options) {
|
|
|
64
110
|
instanceName: toString(req.query.instance),
|
|
65
111
|
tags: toArray(req.query.tag),
|
|
66
112
|
query: toString(req.query.query),
|
|
113
|
+
uid: toString(req.query.uid),
|
|
67
114
|
refresh: toRefresh(req.query.refresh)
|
|
68
115
|
});
|
|
69
116
|
res.json({ items });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { NotAllowedError } from '@backstage/errors';\nimport { parseLabelSelector } from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaService } from './GrafanaService';\n\nconst toArray = (value: unknown): string[] | undefined => {\n if (value === undefined) {\n return undefined;\n }\n return Array.isArray(value) ? value.map(String) : [String(value)];\n};\n\nconst toString = (value: unknown): string | undefined =>\n value === undefined ? undefined : String(value);\n\n// Accepts ?refresh=true, ?refresh=1, and the bare ?refresh flag.\nconst toBoolean = (value: unknown): boolean =>\n value !== undefined && ['true', '1', ''].includes(String(value));\n\n/**\n * Creates the Express router that exposes the read-only Grafana REST API.\n *\n * All routes are relative to the plugin base path (`/api/grafana`).\n *\n * @public\n */\nexport async function createRouter(options: {\n grafanaService: GrafanaService;\n /**\n * Whether callers may force live Grafana reads (default `true`). When\n * `false`, `refresh` query parameters are ignored and the `POST …/refresh`\n * routes respond 403.\n */\n allowOnDemandRefresh?: boolean;\n}): Promise<express.Router> {\n const { grafanaService } = options;\n const allowOnDemandRefresh = options.allowOnDemandRefresh ?? true;\n const router = Router();\n router.use(express.json());\n\n const toRefresh = (value: unknown): boolean =>\n allowOnDemandRefresh && toBoolean(value);\n\n const assertRefreshAllowed = () => {\n if (!allowOnDemandRefresh) {\n throw new NotAllowedError(\n 'On-demand refresh is disabled by configuration (grafana.allowOnDemandRefresh)',\n );\n }\n };\n\n router.get('/health', (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/instances', (_req, res) => {\n res.json({ items: grafanaService.getInstances() });\n });\n\n router.get('/instances/:name/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: req.params.name,\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/instances/:name/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: req.params.name,\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.post('/instances/:name/refresh', async (req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh(req.params.name);\n res.json({ status: 'ok' });\n });\n\n router.get('/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: toString(req.query.instance),\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: toString(req.query.instance),\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.post('/refresh', async (_req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh();\n res.json({ status: 'ok' });\n });\n\n return router;\n}\n"],"names":["Router","express","NotAllowedError","parseLabelSelector"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2026 Cassidy Marble\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport express from 'express';\nimport Router from 'express-promise-router';\nimport { InputError, NotAllowedError, NotFoundError } from '@backstage/errors';\nimport { parseLabelSelector } from '@marble-sh/backstage-plugin-grafana-node';\nimport { GrafanaService } from './GrafanaService';\n\nconst toArray = (value: unknown): string[] | undefined => {\n if (value === undefined) {\n return undefined;\n }\n return Array.isArray(value) ? value.map(String) : [String(value)];\n};\n\nconst toString = (value: unknown): string | undefined =>\n value === undefined ? undefined : String(value);\n\n// Accepts ?refresh=true, ?refresh=1, and the bare ?refresh flag.\nconst toBoolean = (value: unknown): boolean =>\n value !== undefined && ['true', '1', ''].includes(String(value));\n\n/**\n * Creates the Express router that exposes the read-only Grafana REST API.\n *\n * All routes are relative to the plugin base path (`/api/grafana`).\n *\n * @public\n */\nexport async function createRouter(options: {\n grafanaService: GrafanaService;\n /**\n * Whether callers may force live Grafana reads (default `true`). When\n * `false`, `refresh` query parameters are ignored and the `POST …/refresh`\n * routes respond 403.\n */\n allowOnDemandRefresh?: boolean;\n /**\n * Whether the panel routes are served (default `true`). Panel listings and\n * panel data always read live from Grafana, so `false` disables them (403)\n * for deployments that want schedule-only Grafana traffic.\n */\n allowPanelQueries?: boolean;\n}): Promise<express.Router> {\n const { grafanaService } = options;\n const allowOnDemandRefresh = options.allowOnDemandRefresh ?? true;\n const allowPanelQueries = options.allowPanelQueries ?? true;\n const router = Router();\n router.use(express.json());\n\n const toRefresh = (value: unknown): boolean =>\n allowOnDemandRefresh && toBoolean(value);\n\n const assertRefreshAllowed = () => {\n if (!allowOnDemandRefresh) {\n throw new NotAllowedError(\n 'On-demand refresh is disabled by configuration (grafana.allowOnDemandRefresh)',\n );\n }\n };\n\n const assertPanelQueriesAllowed = () => {\n if (!allowPanelQueries) {\n throw new NotAllowedError(\n 'Panel queries are disabled by configuration (grafana.allowPanelQueries)',\n );\n }\n };\n\n router.get('/health', (_req, res) => {\n res.json({ status: 'ok' });\n });\n\n router.get('/instances', (_req, res) => {\n res.json({ items: grafanaService.getInstances() });\n });\n\n router.get('/instances/:name/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: req.params.name,\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n uid: toString(req.query.uid),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/instances/:name/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: req.params.name,\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/instances/:name/dashboards/:uid/panels', async (req, res) => {\n assertPanelQueriesAllowed();\n if (!grafanaService.getPanels) {\n throw new NotFoundError(\n 'Panel queries are not supported by the configured Grafana service',\n );\n }\n const items = await grafanaService.getPanels({\n instanceName: req.params.name,\n dashboardUid: req.params.uid,\n });\n res.json({ items });\n });\n\n router.get(\n '/instances/:name/dashboards/:uid/panels/:panelId/data',\n async (req, res) => {\n assertPanelQueriesAllowed();\n if (!grafanaService.getPanelData) {\n throw new NotFoundError(\n 'Panel queries are not supported by the configured Grafana service',\n );\n }\n if (!/^\\d+$/.test(req.params.panelId)) {\n throw new InputError(\n `Invalid panel id '${req.params.panelId}', expected an integer`,\n );\n }\n const data = await grafanaService.getPanelData({\n instanceName: req.params.name,\n dashboardUid: req.params.uid,\n panelId: Number(req.params.panelId),\n from: toString(req.query.from),\n to: toString(req.query.to),\n });\n res.json(data);\n },\n );\n\n router.post('/instances/:name/refresh', async (req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh(req.params.name);\n res.json({ status: 'ok' });\n });\n\n router.get('/dashboards', async (req, res) => {\n const items = await grafanaService.getDashboards({\n instanceName: toString(req.query.instance),\n tags: toArray(req.query.tag),\n query: toString(req.query.query),\n uid: toString(req.query.uid),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.get('/alerts', async (req, res) => {\n const items = await grafanaService.getAlerts({\n instanceName: toString(req.query.instance),\n labelSelector: parseLabelSelector(toString(req.query.labelSelector)),\n refresh: toRefresh(req.query.refresh),\n });\n res.json({ items });\n });\n\n router.post('/refresh', async (_req, res) => {\n assertRefreshAllowed();\n await grafanaService.refresh();\n res.json({ status: 'ok' });\n });\n\n return router;\n}\n"],"names":["Router","express","NotAllowedError","parseLabelSelector","NotFoundError","InputError"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marble-sh/backstage-plugin-grafana-backend",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Backend for the Grafana Backstage plugin: reads dashboards and alerts from Grafana, caches them, and exposes a read-only REST API",
|
|
5
5
|
"main": "./dist/index.cjs.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"@backstage/config": "^1.3.8",
|
|
38
38
|
"@backstage/errors": "^1.3.1",
|
|
39
39
|
"@backstage/types": "^1.2.2",
|
|
40
|
-
"@marble-sh/backstage-plugin-grafana-common": "^1.0
|
|
41
|
-
"@marble-sh/backstage-plugin-grafana-node": "^1.0
|
|
40
|
+
"@marble-sh/backstage-plugin-grafana-common": "^1.2.0",
|
|
41
|
+
"@marble-sh/backstage-plugin-grafana-node": "^1.2.0",
|
|
42
42
|
"express": "^4.17.1",
|
|
43
43
|
"express-promise-router": "^4.1.0",
|
|
44
44
|
"knex": "^3.0.0"
|