@marble-sh/backstage-plugin-grafana-backend 1.2.0 → 1.3.1
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 +67 -0
- package/README.md +98 -22
- package/dist/index.d.ts +11 -1
- package/dist/plugin.cjs.js +5 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/service/GrafanaService.cjs.js +38 -15
- package/dist/service/GrafanaService.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +4 -2
- package/dist/service/router.cjs.js.map +1 -1
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# @marble-sh/backstage-plugin-grafana-backend
|
|
2
2
|
|
|
3
|
+
## 1.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- b164dcc: Bump backstage dependencies to 1.55.1
|
|
8
|
+
- Updated dependencies [b164dcc]
|
|
9
|
+
- @marble-sh/backstage-plugin-grafana-common@1.2.2
|
|
10
|
+
- @marble-sh/backstage-plugin-grafana-node@1.2.2
|
|
11
|
+
|
|
12
|
+
## 1.3.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 33ad02e: Hardening fixes from a cross-package audit:
|
|
17
|
+
|
|
18
|
+
- **Backend**: a read that fans out over all instances now skips (and logs)
|
|
19
|
+
an unreachable instance instead of failing the whole request; a named
|
|
20
|
+
instance still propagates its error. The panel routes accept
|
|
21
|
+
`refresh=true` (gated by `allowOnDemandRefresh`) to bypass the panel
|
|
22
|
+
cache, and the frontend's "Refresh panels" button uses it — previously
|
|
23
|
+
the button silently served cached data. Startup warns when
|
|
24
|
+
`store: database` is combined with no `schedule` (snapshots would never
|
|
25
|
+
expire).
|
|
26
|
+
- **Frontend**: `GrafanaApi.listPanels`/`getPanelData` are now optional, as
|
|
27
|
+
the changelog already promised — pre-existing custom implementations
|
|
28
|
+
compile again; the dashboards tab falls back to a Grafana link when they
|
|
29
|
+
are absent.
|
|
30
|
+
- **Node library**: the per-instance dashboard-model cache evicts expired
|
|
31
|
+
entries instead of growing forever; an empty `__panelId__` annotation is
|
|
32
|
+
no longer parsed as panel `0`; generated fallback refIds can no longer
|
|
33
|
+
collide with an explicitly declared refId (Grafana keys query results by
|
|
34
|
+
refId); failed _hidden_ queries no longer surface user-facing warnings.
|
|
35
|
+
- **Catalog module**: discovered dashboard entities now carry only
|
|
36
|
+
`grafana/dashboard-uid`, not a title-based `grafana/dashboard-selector` —
|
|
37
|
+
the selectors AND together, so a stale title (dashboard renamed in
|
|
38
|
+
Grafana between discovery runs) hid the dashboard its own uid still
|
|
39
|
+
matched.
|
|
40
|
+
- **Scaffolder module**: `grafana.scaffolder.allowedInstances` is validated
|
|
41
|
+
at startup (like the catalog module) instead of on every run; an
|
|
42
|
+
`overwrite` update reads the current dashboard first, carries its
|
|
43
|
+
`metadata.resourceVersion` into the `PUT`, and degrades to a plain create
|
|
44
|
+
when the dashboard does not exist yet, keeping template runs idempotent.
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- c9c9f23: Documentation: a full entity-annotation reference (exact matching semantics,
|
|
49
|
+
how the annotations combine, visibility gating, and error behavior for
|
|
50
|
+
unknown instance names) in the frontend and common READMEs, and a new
|
|
51
|
+
"Creating the Grafana service account and token" walkthrough in the backend
|
|
52
|
+
README — UI steps, the Grafana Cloud `glsa_` vs `glc_` token distinction,
|
|
53
|
+
and a per-feature permission table (Viewer covers all read paths;
|
|
54
|
+
`datasources:query` caveat for panel graphs under Enterprise/Cloud data
|
|
55
|
+
source permissions; Editor / `fixed:dashboards:writer` for the scaffolder
|
|
56
|
+
module).
|
|
57
|
+
- 9664e66: Documentation consistency pass: the backend README no longer claims the
|
|
58
|
+
catalog module consumes the REST API (it uses the shared node client
|
|
59
|
+
server-side); the scaffolder README/config schema document startup
|
|
60
|
+
validation of `allowedInstances` and the idempotent
|
|
61
|
+
`overwrite` (resourceVersion carry, create fallback); fan-out
|
|
62
|
+
failure-skipping and panel-route `refresh` are documented; and the
|
|
63
|
+
grafana-node README credits all three consumers.
|
|
64
|
+
- Updated dependencies [c9c9f23]
|
|
65
|
+
- Updated dependencies [33ad02e]
|
|
66
|
+
- Updated dependencies [9664e66]
|
|
67
|
+
- @marble-sh/backstage-plugin-grafana-common@1.2.1
|
|
68
|
+
- @marble-sh/backstage-plugin-grafana-node@1.2.1
|
|
69
|
+
|
|
3
70
|
## 1.2.0
|
|
4
71
|
|
|
5
72
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
A read-only [Grafana](https://grafana.com/) backend plugin for Backstage,
|
|
4
4
|
targeting the **new backend system**.
|
|
5
5
|
|
|
6
|
-
The backend performs **all** communication with Grafana. It
|
|
7
|
-
alerts, caches them (in the Backstage cache **or**
|
|
8
|
-
refreshes them on a schedule, and exposes a small
|
|
9
|
-
`/api/grafana`. The frontend plugin
|
|
10
|
-
([`@marble-sh/backstage-plugin-grafana`](../grafana/README.md))
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
The backend performs **all** browser-facing communication with Grafana. It
|
|
7
|
+
reads dashboards and alerts, caches them (in the Backstage cache **or**
|
|
8
|
+
database), optionally refreshes them on a schedule, and exposes a small
|
|
9
|
+
read-only REST API under `/api/grafana`. The frontend plugin
|
|
10
|
+
([`@marble-sh/backstage-plugin-grafana`](../grafana/README.md)) talks only to
|
|
11
|
+
this API and never contacts Grafana; the catalog and scaffolder modules run
|
|
12
|
+
server-side and reach Grafana through the shared
|
|
13
|
+
[`grafana-node`](../grafana-node/README.md) client. Either way, credentials
|
|
14
|
+
never leave the backend.
|
|
13
15
|
|
|
14
16
|
## Features
|
|
15
17
|
|
|
@@ -64,7 +66,10 @@ instance list picks it up.
|
|
|
64
66
|
grafana:
|
|
65
67
|
# Where fetched data is stored between refreshes:
|
|
66
68
|
# cache (default) – ephemeral, honors cacheTtl
|
|
67
|
-
# database – durable, survives restarts and is shared across replicas
|
|
69
|
+
# database – durable, survives restarts and is shared across replicas.
|
|
70
|
+
# Database snapshots never expire, so configure a
|
|
71
|
+
# `schedule` with it — otherwise data only updates on
|
|
72
|
+
# explicit refreshes (the backend warns at startup).
|
|
68
73
|
store: cache
|
|
69
74
|
cacheTtl: { minutes: 15 }
|
|
70
75
|
|
|
@@ -86,7 +91,7 @@ grafana:
|
|
|
86
91
|
- name: production
|
|
87
92
|
title: Production Grafana
|
|
88
93
|
baseUrl: https://grafana.internal.example.com
|
|
89
|
-
token: ${GRAFANA_PROD_TOKEN} # service-account token,
|
|
94
|
+
token: ${GRAFANA_PROD_TOKEN} # service-account token, see "Creating the Grafana service account" below
|
|
90
95
|
|
|
91
96
|
# A Grafana Cloud stack (namespace derived as "stacks-<stackId>").
|
|
92
97
|
- name: cloud
|
|
@@ -98,16 +103,16 @@ grafana:
|
|
|
98
103
|
|
|
99
104
|
### Instance options
|
|
100
105
|
|
|
101
|
-
| Key | Required | Description
|
|
102
|
-
| ---------------- | -------- |
|
|
103
|
-
| `name` | yes | Unique, stable id. Referenced by the `grafana/instance` entity annotation and by the REST API.
|
|
104
|
-
| `baseUrl` | yes | Base URL of the Grafana instance, without a trailing slash.
|
|
105
|
-
| `token` | yes | Service-account token used as a Bearer token
|
|
106
|
-
| `title` | no | Human-readable title. Defaults to `name`.
|
|
107
|
-
| `namespace` | no | App Platform namespace. Defaults to `default` (self-hosted) or `stacks-<stackId>` (cloud).
|
|
108
|
-
| `stackId` | no | **Numeric** Grafana Cloud stack id (not the stack slug), used to derive the namespace (see below).
|
|
109
|
-
| `apis` | no | Override or disable the API used per data type (see below).
|
|
110
|
-
| `resolveFolders` | no | `false` skips the `/api/folders` folder lookup (see below). Defaults to `true`.
|
|
106
|
+
| Key | Required | Description |
|
|
107
|
+
| ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
108
|
+
| `name` | yes | Unique, stable id. Referenced by the `grafana/instance` entity annotation and by the REST API. |
|
|
109
|
+
| `baseUrl` | yes | Base URL of the Grafana instance, without a trailing slash. |
|
|
110
|
+
| `token` | yes | Service-account token used as a Bearer token — see [Creating the Grafana service account and token](#creating-the-grafana-service-account-and-token). Marked secret. |
|
|
111
|
+
| `title` | no | Human-readable title. Defaults to `name`. |
|
|
112
|
+
| `namespace` | no | App Platform namespace. Defaults to `default` (self-hosted) or `stacks-<stackId>` (cloud). |
|
|
113
|
+
| `stackId` | no | **Numeric** Grafana Cloud stack id (not the stack slug), used to derive the namespace (see below). |
|
|
114
|
+
| `apis` | no | Override or disable the API used per data type (see below). |
|
|
115
|
+
| `resolveFolders` | no | `false` skips the `/api/folders` folder lookup (see below). Defaults to `true`. |
|
|
111
116
|
|
|
112
117
|
### API selection
|
|
113
118
|
|
|
@@ -147,8 +152,9 @@ to `true`; each can be set independently.
|
|
|
147
152
|
- **`allowOnDemandRefresh`** — may API callers force live reads?
|
|
148
153
|
|
|
149
154
|
- `true` (default): `?refresh=true` (also `?refresh=1` or the bare flag)
|
|
150
|
-
bypasses the store
|
|
151
|
-
trigger immediate
|
|
155
|
+
bypasses the store (on the panel routes: the panel cache), and
|
|
156
|
+
`POST /refresh` / `POST /instances/:name/refresh` trigger immediate
|
|
157
|
+
refreshes.
|
|
152
158
|
- `false`: `refresh` query parameters are silently ignored (the request is
|
|
153
159
|
served exactly as if the parameter were absent) and both `POST …/refresh`
|
|
154
160
|
routes respond `403 NotAllowedError`. The scheduled refresh is unaffected.
|
|
@@ -201,6 +207,69 @@ is rejected at startup. Two ways to find the id:
|
|
|
201
207
|
# → "stacks-1216502" — the number is the stackId
|
|
202
208
|
```
|
|
203
209
|
|
|
210
|
+
## Creating the Grafana service account and token
|
|
211
|
+
|
|
212
|
+
Every request to Grafana is authenticated with a Grafana **service account
|
|
213
|
+
token** — the `token` of the instance configuration. Create one per instance
|
|
214
|
+
(creating service accounts requires a Grafana organization administrator):
|
|
215
|
+
|
|
216
|
+
1. Sign in to the Grafana instance itself — for Grafana Cloud that is the
|
|
217
|
+
stack's own Grafana at `https://<stack>.grafana.net`, **not** the
|
|
218
|
+
grafana.com portal — and open **Administration → Users and access →
|
|
219
|
+
Service accounts**.
|
|
220
|
+
2. Click **Add service account**, give it a recognizable display name (for
|
|
221
|
+
example `backstage`), and assign it the **Viewer** role (see
|
|
222
|
+
[required permissions](#required-permissions) for when you need more).
|
|
223
|
+
3. On the new service account, click **Add service account token**, name the
|
|
224
|
+
token, preferably set an expiration date, and click **Generate token**.
|
|
225
|
+
4. Copy the generated value — it starts with `glsa_` — into the instance's
|
|
226
|
+
`token` (via an environment variable or another secret source; never
|
|
227
|
+
commit it).
|
|
228
|
+
|
|
229
|
+
> **Grafana Cloud:** the plugin needs a per-stack service-account token
|
|
230
|
+
> (`glsa_…`) created inside the stack's Grafana as above. A Cloud _access
|
|
231
|
+
> policy_ token from the grafana.com portal (`glc_…`) authenticates a
|
|
232
|
+
> different API surface and will not work here.
|
|
233
|
+
|
|
234
|
+
### Required permissions
|
|
235
|
+
|
|
236
|
+
What the token needs depends on the features you use:
|
|
237
|
+
|
|
238
|
+
| Feature (Grafana APIs called) | Required access |
|
|
239
|
+
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
240
|
+
| Dashboard listing and catalog discovery (`dashboard.grafana.app` list, `/api/search`) | `dashboards:read` |
|
|
241
|
+
| Folder titles/links, i.e. `resolveFolders` (`/api/folders`) | `folders:read` |
|
|
242
|
+
| Alerts (`/api/prometheus/grafana/api/v1/rules`) | Grafana-managed alert-rule read (the `fixed:alerting.rules:reader` role) |
|
|
243
|
+
| Panel graphs (dashboard model read + `POST /api/ds/query`) | `dashboards:read` plus `datasources:query` on the queried data sources |
|
|
244
|
+
| [Scaffolder action](../scaffolder-backend-module-grafana/README.md) (`dashboard.grafana.app` create/update) | `dashboards:read` + `dashboards:create` (+ `dashboards:write` for `overwrite`) |
|
|
245
|
+
|
|
246
|
+
In practice:
|
|
247
|
+
|
|
248
|
+
- The **Viewer** basic role covers everything this plugin and the catalog
|
|
249
|
+
module read: dashboards, folders, and Grafana-managed alert rules — and, on
|
|
250
|
+
OSS Grafana (and any instance without data source permissions enforced),
|
|
251
|
+
the panel-data queries too, since viewers query data sources whenever they
|
|
252
|
+
view a dashboard.
|
|
253
|
+
- On **Grafana Enterprise / Cloud with data source permissions**, the Viewer
|
|
254
|
+
basic role does not automatically include `datasources:query` on every data
|
|
255
|
+
source. If panel graphs fail with authorization errors, grant the service
|
|
256
|
+
account query access on the relevant data sources (or the RBAC
|
|
257
|
+
`fixed:datasources:reader` role), or disable the panel routes with
|
|
258
|
+
`grafana.allowPanelQueries: false`.
|
|
259
|
+
- The **scaffolder module writes** dashboards: give that instance's service
|
|
260
|
+
account the **Editor** basic role, or (with RBAC) `fixed:dashboards:writer`
|
|
261
|
+
scoped to the folders your templates target.
|
|
262
|
+
- An instance that serves fewer features needs fewer permissions — e.g. with
|
|
263
|
+
`apis.alerts: none` the alerting API is never called, so alert-rule read
|
|
264
|
+
access is unnecessary.
|
|
265
|
+
|
|
266
|
+
To inspect exactly what a token is allowed to do, ask Grafana itself:
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
curl -s -H "Authorization: Bearer $TOKEN" \
|
|
270
|
+
"$BASE_URL/api/access-control/user/permissions" | jq
|
|
271
|
+
```
|
|
272
|
+
|
|
204
273
|
## REST API
|
|
205
274
|
|
|
206
275
|
All routes are mounted under `/api/grafana` and (except `/health`) require a
|
|
@@ -229,7 +298,9 @@ valid Backstage credential.
|
|
|
229
298
|
- `labelSelector` — `key=value,key2=value2`; only alerts matching **all** pairs.
|
|
230
299
|
- `instance` — (on `/dashboards` and `/alerts`) restrict to a single instance.
|
|
231
300
|
- `refresh` — `true` or `1` (or the bare flag) to bypass the store and read
|
|
232
|
-
live from Grafana.
|
|
301
|
+
live from Grafana. On the panel routes it bypasses the panel cache
|
|
302
|
+
(`panelDataCacheTtl`) instead. Ignored when
|
|
303
|
+
`allowOnDemandRefresh: false`.
|
|
233
304
|
- `from` / `to` — (on the panel data route) the query range, as Grafana time
|
|
234
305
|
expressions: `now`, `now-<n><s|m|h|d|w>`, or epoch milliseconds. Default
|
|
235
306
|
`now-6h` … `now`.
|
|
@@ -253,6 +324,11 @@ Each request resolves to a per-instance snapshot (all dashboards + all alerts):
|
|
|
253
324
|
Because filtering happens after retrieval, a single cached snapshot serves many
|
|
254
325
|
entities with different selectors, keeping Grafana API traffic low.
|
|
255
326
|
|
|
327
|
+
A request that spans **all** instances (no instance in the path or query)
|
|
328
|
+
skips — and logs — any instance that fails to load, so one unreachable
|
|
329
|
+
Grafana cannot fail reads the other instances can still serve. Naming an
|
|
330
|
+
instance explicitly surfaces its error instead.
|
|
331
|
+
|
|
256
332
|
Panel listings and panel data are different: they are inherently live (a graph
|
|
257
333
|
of a stale range is wrong, not cached), so they bypass the snapshot store
|
|
258
334
|
entirely. Instead they are kept in the cache service for a short
|
package/dist/index.d.ts
CHANGED
|
@@ -93,6 +93,8 @@ type GetPanelsOptions = {
|
|
|
93
93
|
instanceName: string;
|
|
94
94
|
/** The uid of the dashboard whose panels are listed. */
|
|
95
95
|
dashboardUid: string;
|
|
96
|
+
/** Force a live read, bypassing the panel cache. */
|
|
97
|
+
refresh?: boolean;
|
|
96
98
|
};
|
|
97
99
|
/**
|
|
98
100
|
* Options for querying the data of a single panel through the service.
|
|
@@ -110,6 +112,8 @@ type GetPanelDataOptions = {
|
|
|
110
112
|
from?: string;
|
|
111
113
|
/** Range end, same forms as `from`. Defaults to `now`. */
|
|
112
114
|
to?: string;
|
|
115
|
+
/** Force a live read, bypassing the panel cache. */
|
|
116
|
+
refresh?: boolean;
|
|
113
117
|
};
|
|
114
118
|
/**
|
|
115
119
|
* Reads dashboards and alerts from the configured Grafana instances, backed by a
|
|
@@ -179,7 +183,13 @@ declare class DefaultGrafanaService implements GrafanaService {
|
|
|
179
183
|
/** {@inheritDoc GrafanaService.getPanelData} */
|
|
180
184
|
getPanelData(options: GetPanelDataOptions): Promise<GrafanaPanelData>;
|
|
181
185
|
private withPanelCache;
|
|
182
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Resolves the snapshots a read spans. A read of one named instance
|
|
188
|
+
* propagates that instance's failure; a fan-out over all instances skips
|
|
189
|
+
* (and logs) failing instances instead, so one unreachable Grafana cannot
|
|
190
|
+
* fail reads that other instances can still serve.
|
|
191
|
+
*/
|
|
192
|
+
private snapshotsFor;
|
|
183
193
|
private mustGet;
|
|
184
194
|
private snapshotFor;
|
|
185
195
|
private refreshInstance;
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -27,6 +27,11 @@ const grafanaPlugin = backendPluginApi.createBackendPlugin({
|
|
|
27
27
|
"No Grafana instances are configured under `grafana.instances`; the grafana plugin will return empty results"
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
|
+
if (grafanaConfig.store === "database" && !grafanaConfig.schedule) {
|
|
31
|
+
logger.warn(
|
|
32
|
+
"grafana.store is `database` but no `grafana.schedule` is configured; stored snapshots never expire, so data will only update on explicit refreshes (`?refresh=true` or `POST \u2026/refresh`)"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
30
35
|
const store = grafanaConfig.store === "database" ? await DatabaseGrafanaStore.DatabaseGrafanaStore.create({ database }) : new CacheGrafanaStore.CacheGrafanaStore({ cache, ttl: grafanaConfig.cacheTtl });
|
|
31
36
|
const instances = grafanaConfig.instances.map(
|
|
32
37
|
(instance) => ({
|
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 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
|
+
{"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 if (grafanaConfig.store === 'database' && !grafanaConfig.schedule) {\n logger.warn(\n 'grafana.store is `database` but no `grafana.schedule` is configured; ' +\n 'stored snapshots never expire, so data will only update on ' +\n 'explicit refreshes (`?refresh=true` or `POST …/refresh`)',\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,IAAI,aAAA,CAAc,KAAA,KAAU,UAAA,IAAc,CAAC,cAAc,QAAA,EAAU;AACjE,UAAA,MAAA,CAAO,IAAA;AAAA,YACL;AAAA,WAGF;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;;"}
|
|
@@ -33,10 +33,11 @@ class DefaultGrafanaService {
|
|
|
33
33
|
}
|
|
34
34
|
/** {@inheritDoc GrafanaService.getDashboards} */
|
|
35
35
|
async getDashboards(options) {
|
|
36
|
-
const names = this.resolveNames(options.instanceName);
|
|
37
36
|
const result = [];
|
|
38
|
-
for (const
|
|
39
|
-
|
|
37
|
+
for (const snapshot of await this.snapshotsFor(
|
|
38
|
+
options.instanceName,
|
|
39
|
+
options.refresh
|
|
40
|
+
)) {
|
|
40
41
|
result.push(
|
|
41
42
|
...backstagePluginGrafanaNode.filterDashboards(snapshot.dashboards, {
|
|
42
43
|
tags: options.tags,
|
|
@@ -49,10 +50,11 @@ class DefaultGrafanaService {
|
|
|
49
50
|
}
|
|
50
51
|
/** {@inheritDoc GrafanaService.getAlerts} */
|
|
51
52
|
async getAlerts(options) {
|
|
52
|
-
const names = this.resolveNames(options.instanceName);
|
|
53
53
|
const result = [];
|
|
54
|
-
for (const
|
|
55
|
-
|
|
54
|
+
for (const snapshot of await this.snapshotsFor(
|
|
55
|
+
options.instanceName,
|
|
56
|
+
options.refresh
|
|
57
|
+
)) {
|
|
56
58
|
result.push(
|
|
57
59
|
...backstagePluginGrafanaNode.filterAlerts(snapshot.alerts, {
|
|
58
60
|
labelSelector: options.labelSelector
|
|
@@ -88,7 +90,8 @@ class DefaultGrafanaService {
|
|
|
88
90
|
}
|
|
89
91
|
return this.withPanelCache(
|
|
90
92
|
`panels:v1:${options.instanceName}:${options.dashboardUid}`,
|
|
91
|
-
() => client.getPanels(options.dashboardUid)
|
|
93
|
+
() => client.getPanels(options.dashboardUid),
|
|
94
|
+
options.refresh
|
|
92
95
|
);
|
|
93
96
|
}
|
|
94
97
|
/** {@inheritDoc GrafanaService.getPanelData} */
|
|
@@ -106,27 +109,47 @@ class DefaultGrafanaService {
|
|
|
106
109
|
() => client.getPanelData(options.dashboardUid, options.panelId, {
|
|
107
110
|
from,
|
|
108
111
|
to
|
|
109
|
-
})
|
|
112
|
+
}),
|
|
113
|
+
options.refresh
|
|
110
114
|
);
|
|
111
115
|
}
|
|
112
|
-
async withPanelCache(key, fn) {
|
|
116
|
+
async withPanelCache(key, fn, refresh) {
|
|
113
117
|
if (!this.cache) {
|
|
114
118
|
return fn();
|
|
115
119
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
if (!refresh) {
|
|
121
|
+
const cached = await this.cache.get(key);
|
|
122
|
+
if (cached !== void 0) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
119
125
|
}
|
|
120
126
|
const value = await fn();
|
|
121
127
|
await this.cache.set(key, value, { ttl: this.panelCacheTtlMs });
|
|
122
128
|
return value;
|
|
123
129
|
}
|
|
124
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Resolves the snapshots a read spans. A read of one named instance
|
|
132
|
+
* propagates that instance's failure; a fan-out over all instances skips
|
|
133
|
+
* (and logs) failing instances instead, so one unreachable Grafana cannot
|
|
134
|
+
* fail reads that other instances can still serve.
|
|
135
|
+
*/
|
|
136
|
+
async snapshotsFor(instanceName, refresh) {
|
|
125
137
|
if (instanceName) {
|
|
126
138
|
this.mustGet(instanceName);
|
|
127
|
-
return [instanceName];
|
|
139
|
+
return [await this.snapshotFor(instanceName, refresh)];
|
|
140
|
+
}
|
|
141
|
+
const snapshots = [];
|
|
142
|
+
for (const name of this.instances.keys()) {
|
|
143
|
+
try {
|
|
144
|
+
snapshots.push(await this.snapshotFor(name, refresh));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.logger.warn(
|
|
147
|
+
`Failed to read Grafana instance '${name}'; skipping it for this request`,
|
|
148
|
+
error
|
|
149
|
+
);
|
|
150
|
+
}
|
|
128
151
|
}
|
|
129
|
-
return
|
|
152
|
+
return snapshots;
|
|
130
153
|
}
|
|
131
154
|
mustGet(instanceName) {
|
|
132
155
|
const instance = this.instances.get(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 { 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":";;;;;;AAuIO,MAAM,qBAAA,CAAgD;AAAA,EAC1C,SAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,aAAA;AAAA,EACA,KAAA;AAAA,EACA,eAAA;AAAA,EAEjB,YAAY,OAAA,EAmBT;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;AAC9C,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,KAAA;AACrB,IAAA,IAAA,CAAK,eAAA,GAAkBA,4BAAA;AAAA,MACrB,OAAA,CAAQ,iBAAA,IAAqB,EAAE,OAAA,EAAS,EAAA;AAAG,KAC7C;AAAA,EACF;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,GAAGC,2CAAA,CAAiB,QAAA,CAAS,UAAA,EAAY;AAAA,UACvC,MAAM,OAAA,CAAQ,IAAA;AAAA,UACd,OAAO,OAAA,CAAQ,KAAA;AAAA,UACf,KAAK,OAAA,CAAQ;AAAA,SACd;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;AAAA,EAGA,MAAM,UAAU,OAAA,EAAoD;AAClE,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,IAAA,CAAK,OAAA,CAAQ,QAAQ,YAAY,CAAA;AACpD,IAAA,IAAI,CAAC,OAAO,SAAA,EAAW;AACrB,MAAA,MAAM,IAAIC,oBAAA;AAAA,QACR,CAAA,iCAAA,EAAoC,QAAQ,YAAY,CAAA,gCAAA;AAAA,OAC1D;AAAA,IACF;AACA,IAAA,OAAO,IAAA,CAAK,cAAA;AAAA,MACV,CAAA,UAAA,EAAa,OAAA,CAAQ,YAAY,CAAA,CAAA,EAAI,QAAQ,YAAY,CAAA,CAAA;AAAA,MACzD,MAAM,MAAA,CAAO,SAAA,CAAW,OAAA,CAAQ,YAAY;AAAA,KAC9C;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,aAAa,OAAA,EAAyD;AAC1E,IAAA,MAAM,EAAE,MAAA,EAAO,GAAI,IAAA,CAAK,OAAA,CAAQ,QAAQ,YAAY,CAAA;AACpD,IAAA,IAAI,CAAC,OAAO,YAAA,EAAc;AACxB,MAAA,MAAM,IAAIA,oBAAA;AAAA,QACR,CAAA,iCAAA,EAAoC,QAAQ,YAAY,CAAA,gCAAA;AAAA,OAC1D;AAAA,IACF;AACA,IAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,IAAQ,QAAA;AAC7B,IAAA,MAAM,EAAA,GAAK,QAAQ,EAAA,IAAM,KAAA;AACzB,IAAA,OAAO,IAAA,CAAK,cAAA;AAAA,MACV,CAAA,cAAA,EAAiB,OAAA,CAAQ,YAAY,CAAA,CAAA,EAAI,OAAA,CAAQ,YAAY,CAAA,CAAA,EAAI,OAAA,CAAQ,OAAO,CAAA,CAAA,EAAI,IAAI,CAAA,CAAA,EAAI,EAAE,CAAA,CAAA;AAAA,MAC9F,MACE,MAAA,CAAO,YAAA,CAAc,OAAA,CAAQ,YAAA,EAAc,QAAQ,OAAA,EAAS;AAAA,QAC1D,IAAA;AAAA,QACA;AAAA,OACD;AAAA,KACL;AAAA,EACF;AAAA,EAEA,MAAc,cAAA,CACZ,GAAA,EACA,EAAA,EACY;AACZ,IAAA,IAAI,CAAC,KAAK,KAAA,EAAO;AACf,MAAA,OAAO,EAAA,EAAG;AAAA,IACZ;AACA,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA,CAAM,IAAI,GAAG,CAAA;AACvC,IAAA,IAAI,WAAW,MAAA,EAAW;AACxB,MAAA,OAAO,MAAA;AAAA,IACT;AACA,IAAA,MAAM,KAAA,GAAQ,MAAM,EAAA,EAAG;AACvB,IAAA,MAAM,IAAA,CAAK,MAAM,GAAA,CAAI,GAAA,EAAK,OAAgB,EAAE,GAAA,EAAK,IAAA,CAAK,eAAA,EAAiB,CAAA;AACvE,IAAA,OAAO,KAAA;AAAA,EACT;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,IAAIA,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 /** Force a live read, bypassing the panel cache. */\n refresh?: boolean;\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 /** Force a live read, bypassing the panel cache. */\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 * 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 result: GrafanaDashboard[] = [];\n for (const snapshot of await this.snapshotsFor(\n options.instanceName,\n options.refresh,\n )) {\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 result: GrafanaAlert[] = [];\n for (const snapshot of await this.snapshotsFor(\n options.instanceName,\n options.refresh,\n )) {\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 options.refresh,\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 options.refresh,\n );\n }\n\n private async withPanelCache<T>(\n key: string,\n fn: () => Promise<T>,\n refresh?: boolean,\n ): Promise<T> {\n if (!this.cache) {\n return fn();\n }\n if (!refresh) {\n const cached = await this.cache.get(key);\n if (cached !== undefined) {\n return cached as T;\n }\n }\n const value = await fn();\n await this.cache.set(key, value as never, { ttl: this.panelCacheTtlMs });\n return value;\n }\n\n /**\n * Resolves the snapshots a read spans. A read of one named instance\n * propagates that instance's failure; a fan-out over all instances skips\n * (and logs) failing instances instead, so one unreachable Grafana cannot\n * fail reads that other instances can still serve.\n */\n private async snapshotsFor(\n instanceName: string | undefined,\n refresh?: boolean,\n ): Promise<Array<Pick<GrafanaSnapshot, 'dashboards' | 'alerts'>>> {\n if (instanceName) {\n this.mustGet(instanceName);\n return [await this.snapshotFor(instanceName, refresh)];\n }\n const snapshots: Array<Pick<GrafanaSnapshot, 'dashboards' | 'alerts'>> = [];\n for (const name of this.instances.keys()) {\n try {\n snapshots.push(await this.snapshotFor(name, refresh));\n } catch (error) {\n this.logger.warn(\n `Failed to read Grafana instance '${name}'; skipping it for this request`,\n error as Error,\n );\n }\n }\n return snapshots;\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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"}
|
|
@@ -72,7 +72,8 @@ async function createRouter(options) {
|
|
|
72
72
|
}
|
|
73
73
|
const items = await grafanaService.getPanels({
|
|
74
74
|
instanceName: req.params.name,
|
|
75
|
-
dashboardUid: req.params.uid
|
|
75
|
+
dashboardUid: req.params.uid,
|
|
76
|
+
refresh: toRefresh(req.query.refresh)
|
|
76
77
|
});
|
|
77
78
|
res.json({ items });
|
|
78
79
|
});
|
|
@@ -95,7 +96,8 @@ async function createRouter(options) {
|
|
|
95
96
|
dashboardUid: req.params.uid,
|
|
96
97
|
panelId: Number(req.params.panelId),
|
|
97
98
|
from: toString(req.query.from),
|
|
98
|
-
to: toString(req.query.to)
|
|
99
|
+
to: toString(req.query.to),
|
|
100
|
+
refresh: toRefresh(req.query.refresh)
|
|
99
101
|
});
|
|
100
102
|
res.json(data);
|
|
101
103
|
}
|
|
@@ -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 { 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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"}
|
|
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 refresh: toRefresh(req.query.refresh),\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 refresh: toRefresh(req.query.refresh),\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":";;;;;;;;;;;;AAsBA,MAAM,OAAA,GAAU,CAAC,KAAA,KAAyC;AACxD,EAAA,IAAI,UAAU,MAAA,EAAW;AACvB,IAAA,OAAO,MAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA,CAAM,GAAA,CAAI,MAAM,CAAA,GAAI,CAAC,MAAA,CAAO,KAAK,CAAC,CAAA;AAClE,CAAA;AAEA,MAAM,WAAW,CAAC,KAAA,KAChB,UAAU,MAAA,GAAY,MAAA,GAAY,OAAO,KAAK,CAAA;AAGhD,MAAM,SAAA,GAAY,CAAC,KAAA,KACjB,KAAA,KAAU,MAAA,IAAa,CAAC,MAAA,EAAQ,GAAA,EAAK,EAAE,CAAA,CAAE,QAAA,CAAS,MAAA,CAAO,KAAK,CAAC,CAAA;AASjE,eAAsB,aAAa,OAAA,EAcP;AAC1B,EAAA,MAAM,EAAE,gBAAe,GAAI,OAAA;AAC3B,EAAA,MAAM,oBAAA,GAAuB,QAAQ,oBAAA,IAAwB,IAAA;AAC7D,EAAA,MAAM,iBAAA,GAAoB,QAAQ,iBAAA,IAAqB,IAAA;AACvD,EAAA,MAAM,SAASA,uBAAA,EAAO;AACtB,EAAA,MAAA,CAAO,GAAA,CAAIC,wBAAA,CAAQ,IAAA,EAAM,CAAA;AAEzB,EAAA,MAAM,SAAA,GAAY,CAAC,KAAA,KACjB,oBAAA,IAAwB,UAAU,KAAK,CAAA;AAEzC,EAAA,MAAM,uBAAuB,MAAM;AACjC,IAAA,IAAI,CAAC,oBAAA,EAAsB;AACzB,MAAA,MAAM,IAAIC,sBAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,4BAA4B,MAAM;AACtC,IAAA,IAAI,CAAC,iBAAA,EAAmB;AACtB,MAAA,MAAM,IAAIA,sBAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAAA,EACF,CAAA;AAEA,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,CAAC,IAAA,EAAM,GAAA,KAAQ;AACnC,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,YAAA,EAAc,CAAC,IAAA,EAAM,GAAA,KAAQ;AACtC,IAAA,GAAA,CAAI,KAAK,EAAE,KAAA,EAAO,cAAA,CAAe,YAAA,IAAgB,CAAA;AAAA,EACnD,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,6BAAA,EAA+B,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC5D,IAAA,MAAM,KAAA,GAAQ,MAAM,cAAA,CAAe,aAAA,CAAc;AAAA,MAC/C,YAAA,EAAc,IAAI,MAAA,CAAO,IAAA;AAAA,MACzB,IAAA,EAAM,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA;AAAA,MAC3B,KAAA,EAAO,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,KAAK,CAAA;AAAA,MAC/B,GAAA,EAAK,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA;AAAA,MAC3B,OAAA,EAAS,SAAA,CAAU,GAAA,CAAI,KAAA,CAAM,OAAO;AAAA,KACrC,CAAA;AACD,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA;AAAA,EACpB,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,yBAAA,EAA2B,OAAO,GAAA,EAAK,GAAA,KAAQ;AACxD,IAAA,MAAM,KAAA,GAAQ,MAAM,cAAA,CAAe,SAAA,CAAU;AAAA,MAC3C,YAAA,EAAc,IAAI,MAAA,CAAO,IAAA;AAAA,MACzB,eAAeC,6CAAA,CAAmB,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,aAAa,CAAC,CAAA;AAAA,MACnE,OAAA,EAAS,SAAA,CAAU,GAAA,CAAI,KAAA,CAAM,OAAO;AAAA,KACrC,CAAA;AACD,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA;AAAA,EACpB,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,yCAAA,EAA2C,OAAO,GAAA,EAAK,GAAA,KAAQ;AACxE,IAAA,yBAAA,EAA0B;AAC1B,IAAA,IAAI,CAAC,eAAe,SAAA,EAAW;AAC7B,MAAA,MAAM,IAAIC,oBAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,MAAM,cAAA,CAAe,SAAA,CAAU;AAAA,MAC3C,YAAA,EAAc,IAAI,MAAA,CAAO,IAAA;AAAA,MACzB,YAAA,EAAc,IAAI,MAAA,CAAO,GAAA;AAAA,MACzB,OAAA,EAAS,SAAA,CAAU,GAAA,CAAI,KAAA,CAAM,OAAO;AAAA,KACrC,CAAA;AACD,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA;AAAA,EACpB,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA;AAAA,IACL,uDAAA;AAAA,IACA,OAAO,KAAK,GAAA,KAAQ;AAClB,MAAA,yBAAA,EAA0B;AAC1B,MAAA,IAAI,CAAC,eAAe,YAAA,EAAc;AAChC,QAAA,MAAM,IAAIA,oBAAA;AAAA,UACR;AAAA,SACF;AAAA,MACF;AACA,MAAA,IAAI,CAAC,OAAA,CAAQ,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,OAAO,CAAA,EAAG;AACrC,QAAA,MAAM,IAAIC,iBAAA;AAAA,UACR,CAAA,kBAAA,EAAqB,GAAA,CAAI,MAAA,CAAO,OAAO,CAAA,sBAAA;AAAA,SACzC;AAAA,MACF;AACA,MAAA,MAAM,IAAA,GAAO,MAAM,cAAA,CAAe,YAAA,CAAa;AAAA,QAC7C,YAAA,EAAc,IAAI,MAAA,CAAO,IAAA;AAAA,QACzB,YAAA,EAAc,IAAI,MAAA,CAAO,GAAA;AAAA,QACzB,OAAA,EAAS,MAAA,CAAO,GAAA,CAAI,MAAA,CAAO,OAAO,CAAA;AAAA,QAClC,IAAA,EAAM,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,IAAI,CAAA;AAAA,QAC7B,EAAA,EAAI,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,EAAE,CAAA;AAAA,QACzB,OAAA,EAAS,SAAA,CAAU,GAAA,CAAI,KAAA,CAAM,OAAO;AAAA,OACrC,CAAA;AACD,MAAA,GAAA,CAAI,KAAK,IAAI,CAAA;AAAA,IACf;AAAA,GACF;AAEA,EAAA,MAAA,CAAO,IAAA,CAAK,0BAAA,EAA4B,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC1D,IAAA,oBAAA,EAAqB;AACrB,IAAA,MAAM,cAAA,CAAe,OAAA,CAAQ,GAAA,CAAI,MAAA,CAAO,IAAI,CAAA;AAC5C,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,aAAA,EAAe,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC5C,IAAA,MAAM,KAAA,GAAQ,MAAM,cAAA,CAAe,aAAA,CAAc;AAAA,MAC/C,YAAA,EAAc,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,QAAQ,CAAA;AAAA,MACzC,IAAA,EAAM,OAAA,CAAQ,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA;AAAA,MAC3B,KAAA,EAAO,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,KAAK,CAAA;AAAA,MAC/B,GAAA,EAAK,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,GAAG,CAAA;AAAA,MAC3B,OAAA,EAAS,SAAA,CAAU,GAAA,CAAI,KAAA,CAAM,OAAO;AAAA,KACrC,CAAA;AACD,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA;AAAA,EACpB,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,SAAA,EAAW,OAAO,GAAA,EAAK,GAAA,KAAQ;AACxC,IAAA,MAAM,KAAA,GAAQ,MAAM,cAAA,CAAe,SAAA,CAAU;AAAA,MAC3C,YAAA,EAAc,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,QAAQ,CAAA;AAAA,MACzC,eAAeF,6CAAA,CAAmB,QAAA,CAAS,GAAA,CAAI,KAAA,CAAM,aAAa,CAAC,CAAA;AAAA,MACnE,OAAA,EAAS,SAAA,CAAU,GAAA,CAAI,KAAA,CAAM,OAAO;AAAA,KACrC,CAAA;AACD,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA;AAAA,EACpB,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,IAAA,CAAK,UAAA,EAAY,OAAO,IAAA,EAAM,GAAA,KAAQ;AAC3C,IAAA,oBAAA,EAAqB;AACrB,IAAA,MAAM,eAAe,OAAA,EAAQ;AAC7B,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marble-sh/backstage-plugin-grafana-backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
"test": "backstage-cli package test"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@backstage/backend-plugin-api": "^1.10.
|
|
37
|
-
"@backstage/config": "^1.3.
|
|
36
|
+
"@backstage/backend-plugin-api": "^1.10.1",
|
|
37
|
+
"@backstage/config": "^1.3.9",
|
|
38
38
|
"@backstage/errors": "^1.3.1",
|
|
39
39
|
"@backstage/types": "^1.2.2",
|
|
40
|
-
"@marble-sh/backstage-plugin-grafana-common": "^1.2.
|
|
41
|
-
"@marble-sh/backstage-plugin-grafana-node": "^1.2.
|
|
40
|
+
"@marble-sh/backstage-plugin-grafana-common": "^1.2.2",
|
|
41
|
+
"@marble-sh/backstage-plugin-grafana-node": "^1.2.2",
|
|
42
42
|
"express": "^4.17.1",
|
|
43
43
|
"express-promise-router": "^4.1.0",
|
|
44
44
|
"knex": "^3.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@backstage/backend-test-utils": "^1.11.
|
|
48
|
-
"@backstage/cli": "^0.36.
|
|
47
|
+
"@backstage/backend-test-utils": "^1.11.7",
|
|
48
|
+
"@backstage/cli": "^0.36.6",
|
|
49
49
|
"@types/express": "^4.17.6",
|
|
50
50
|
"@types/supertest": "^2.0.12",
|
|
51
51
|
"supertest": "^6.2.4"
|