Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions workspaces/grafana/.changeset/grafana-multiple-instances.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@backstage-community/plugin-grafana': minor
---

Added support for multiple Grafana instances. Organizations with multiple Grafana deployments can now configure them all under the `grafana.hosts` config key and associate entities to specific instances via the `grafana/host-id` annotation. The legacy single-instance `grafana.domain` configuration remains fully supported.
50 changes: 49 additions & 1 deletion workspaces/grafana/plugins/grafana/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,80 @@ export interface Config {
/**
* Domain used by users to access Grafana web UI.
* Example: https://monitoring.eu.my-company.com/
* Either `domain` or `hosts` must be defined.
* @deprecated Use `grafana.hosts[].domain` in `grafana.hosts` instead.
* @visibility frontend
*/
domain: string;
domain?: string;

/**
* Path to use for requests via the proxy, defaults to /grafana/api
* @deprecated Use `grafana.hosts[].proxyPath` in `grafana.hosts` instead.
* @visibility frontend
*/
proxyPath?: string;

/**
* Is Grafana using unified alerting?
* @deprecated Use `grafana.hosts[].unifiedAlerting` in `grafana.hosts` instead.
* @visibility frontend
*/
unifiedAlerting?: boolean;
Comment thread
andreahlert marked this conversation as resolved.

/**
* Limit value to pass in Grafana Dashboard search query.
* @deprecated Use server-side filtering instead. See https://github.com/backstage/community-plugins/pull/3909
Comment thread
andreahlert marked this conversation as resolved.
* @visibility frontend
*/
grafanaDashboardSearchLimit?: number;

/**
* Max pages of Grafana Dashboard search query to fetch.
* @deprecated Use server-side filtering instead. See https://github.com/backstage/community-plugins/pull/3909
Comment thread
andreahlert marked this conversation as resolved.
* @visibility frontend
*/
grafanaDashboardMaxPages?: number;

/**
* Default host id for entities that do not have the `grafana/host-id` annotation.
* Must match one of the `id` values in `grafana.hosts`.
* When not set, the first host in `hosts` is used (or the legacy `default` host).
* @visibility frontend
*/
defaultHost?: string;

/**
* List of Grafana instances to connect to.
* Either `domain` or `hosts` must be defined.
* @visibility frontend
*/
hosts?: Array<{
/**
* Unique identifier for this Grafana instance.
* Used in the `grafana/host-id` entity annotation.
* @visibility frontend
*/
id: string;

/**
* Domain used by users to access this Grafana instance.
* Example: https://monitoring.eu.my-company.com/
* @visibility frontend
*/
domain: string;

/**
* Path to use for requests via the proxy for this instance.
* Defaults to /grafana/api
* @visibility frontend
*/
proxyPath?: string;

/**
* Is this Grafana instance using unified alerting?
* @visibility frontend
*/
unifiedAlerting?: boolean;
}>;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ annotations:

The `EntityGrafanaAlertsCard` component will then display alerts matching the given label and value.

### Multiple Grafana instances

If you have [multiple Grafana instances configured](setup.md#multiple-instances-configuration), add the `grafana/host-id` annotation to select which instance to query:

```yaml
annotations:
grafana/alert-label-selector: 'service=my-service'
grafana/host-id: production
```

## With Grafana Legacy Alerting enabled

If Grafana's [Unified Alerting](https://grafana.com/blog/2021/06/14/the-new-unified-alerting-system-for-grafana-everything-you-need-to-know/) is NOT enabled, alerts are selected by a tag present on the dashboards defining them:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ const overviewContent = (
Grafana dashboards are correlated to Backstage entities using a selector defined by an annotation added in the entity's `catalog-info.yaml` file.
The `EntityGrafanaDashboardsCard` component will then display dashboards matching the given selector.

If you have [multiple Grafana instances configured](setup.md#multiple-instances-configuration), add the `grafana/host-id` annotation to select which instance to query:

```yaml
annotations:
grafana/dashboard-selector: my-service
grafana/host-id: production
```

The following selector will return dashboards that have a `my-service` or a `my-service-slo` tag and have a `generated` tag.

```yml
Expand Down
55 changes: 55 additions & 0 deletions workspaces/grafana/plugins/grafana/docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Configure the plugin in `app-config.yaml`. The proxy endpoint described below wi
to authenticate with Grafana without exposing your API key to users.
[Create an API key](https://grafana.com/docs/grafana/latest/http_api/auth/#create-api-token) if you don't already have one. `Viewer` access will be enough.

## Single instance configuration

```yaml
# app-config.yaml
proxy:
Expand Down Expand Up @@ -42,6 +44,59 @@ Integrators with 1000-5000 Grafana dashboards should prefer raising the
`grafanaDashboardMaxPages`; as Grafana caches the underlying dashboard listing
endpoint heavily.

## Multiple instances configuration

If your organization has multiple Grafana instances, you can configure them all under the `hosts` key:

```yaml
# app-config.yaml
proxy:
'/grafana/production/api':
target: https://grafana-prod.host/
headers:
Authorization: Bearer ${GRAFANA_PROD_TOKEN}
'/grafana/staging/api':
target: https://grafana-staging.host/
headers:
Authorization: Bearer ${GRAFANA_STAGING_TOKEN}

grafana:
# Optional: default Grafana instance for entities without grafana/host-id (must match a host id below)
defaultHost: production
hosts:
- id: production
domain: https://monitoring-prod.company.com
proxyPath: /grafana/production/api
unifiedAlerting: true
- id: staging
domain: https://monitoring-staging.company.com
proxyPath: /grafana/staging/api
unifiedAlerting: false
```

Each host entry supports `domain`, `proxyPath`, and `unifiedAlerting`, plus a required `id` field that uniquely identifies the instance.

You can set `defaultHost` to the id of the Grafana instance that entities without the `grafana/host-id` annotation should use. If `defaultHost` is not set, the plugin uses the first host in `hosts` (or the `default` host when using the legacy `domain` config).

To associate an entity with a specific Grafana instance, add the `grafana/host-id` annotation to your entity's `catalog-info.yaml`:

Comment thread
andreahlert marked this conversation as resolved.
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-service
annotations:
grafana/host-id: production
grafana/dashboard-selector: my-service
grafana/alert-label-selector: service=my-service
```

If the `grafana/host-id` annotation is not set, the plugin will use the host specified by `grafana.defaultHost`, or the first configured host (or the `default` host created from the legacy `domain` config) when `defaultHost` is not set.

Note: if both `domain` and `hosts` are defined, the `domain` value will be ignored and a warning will be logged.

## Expose the plugin

Expose the plugin to Backstage:

```ts
Expand Down
34 changes: 32 additions & 2 deletions workspaces/grafana/plugins/grafana/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
```ts
import { ApiRef } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { DiscoveryApi } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { FetchApi } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react/jsx-runtime';

// @public
Expand Down Expand Up @@ -76,24 +78,52 @@ export const GRAFANA_ANNOTATION_ALERT_LABEL_SELECTOR =
export const GRAFANA_ANNOTATION_DASHBOARD_SELECTOR =
'grafana/dashboard-selector';

// @public
export const GRAFANA_ANNOTATION_HOST_ID = 'grafana/host-id';

// @public
export const GRAFANA_ANNOTATION_OVERVIEW_DASHBOARD =
'grafana/overview-dashboard';

// @public
export interface GrafanaApi {
alertsForSelector(selectors: string | string[]): Promise<Alert[]>;
listDashboards(query: string): Promise<Dashboard[]>;
alertsForSelector(
selectors: string | string[],
hostId?: string,
): Promise<Alert[]>;
isUnifiedAlerting(hostId?: string): boolean;
listDashboards(query: string, hostId?: string): Promise<Dashboard[]>;
}

// @public
export type GrafanaApiClientOptions = {
discoveryApi: DiscoveryApi;
fetchApi: FetchApi;
hosts: GrafanaHost[];
defaultHostId?: string;
dashboardSearchLimit?: number;
dashboardMaxPages?: number;
};

// @public
export const grafanaApiRef: ApiRef<GrafanaApi, 'plugin.grafana.service'> & {
readonly $$type: '@backstage/ApiRef';
};

// @public
export interface GrafanaHost {
domain: string;
id: string;
proxyPath?: string;
unifiedAlerting?: boolean;
}

// @public
export const grafanaPlugin: BackstagePlugin<{}, {}, {}>;

// @public
export const hostIdFromEntity: (entity: Entity) => string | undefined;

// @public
export const isAlertSelectorAvailable: (entity: Entity) => boolean;

Expand Down
21 changes: 21 additions & 0 deletions workspaces/grafana/plugins/grafana/src/__fixtures__/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
GRAFANA_ANNOTATION_ALERT_LABEL_SELECTOR,
GRAFANA_ANNOTATION_OVERVIEW_DASHBOARD,
GRAFANA_ANNOTATION_DASHBOARD_SELECTOR,
GRAFANA_ANNOTATION_HOST_ID,
} from '../constants';

export const sampleEntity = {
Expand All @@ -40,3 +41,23 @@ export const sampleEntity = {
},
},
};

export const sampleEntityWithHostId = {
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
metadata: {
name: 'awesome-service-prod',
annotations: {
[GRAFANA_ANNOTATION_ALERT_LABEL_SELECTOR]: 'service=awesome-service',
[GRAFANA_ANNOTATION_DASHBOARD_SELECTOR]: 'awesome-service',
[GRAFANA_ANNOTATION_HOST_ID]: 'production',
},
},
spec: {
lifecycle: 'production',
type: 'service',
owner: 'cncf',
},
},
};
38 changes: 11 additions & 27 deletions workspaces/grafana/plugins/grafana/src/alpha/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ import {
fetchApiRef,
identityApiRef,
} from '@backstage/frontend-plugin-api';
import {
GrafanaApiClient,
grafanaApiRef,
UnifiedAlertingGrafanaApiClient,
} from '../api';
import { GrafanaApiClient, grafanaApiRef } from '../api';
import { readHosts } from '../config';

/**
* @alpha
Expand All @@ -40,33 +37,20 @@ export const grafanaApiExtension = ApiBlueprint.make({
fetchApi: fetchApiRef,
},
factory: ({ discoveryApi, configApi, fetchApi }) => {
const unifiedAlertingEnabled =
configApi.getOptionalBoolean('grafana.unifiedAlerting') || false;

if (!unifiedAlertingEnabled) {
return new GrafanaApiClient({
fetchApi,
discoveryApi,
domain: configApi.getString('grafana.domain'),
proxyPath: configApi.getOptionalString('grafana.proxyPath'),
grafanaDashboardSearchLimit: configApi.getOptionalNumber(
'grafana.grafanaDashboardSearchLimit',
),
grafanaDashboardMaxPages: configApi.getOptionalNumber(
'grafana.grafanaDashboardMaxPages',
),
});
}
const { hosts, defaultHostId } = readHosts(configApi);

return new UnifiedAlertingGrafanaApiClient({
return new GrafanaApiClient({
fetchApi,
discoveryApi,
domain: configApi.getString('grafana.domain'),
proxyPath: configApi.getOptionalString('grafana.proxyPath'),
grafanaDashboardSearchLimit: configApi.getOptionalNumber(
hosts,
defaultHostId,
// Legacy config (backward compatibility); see config.d.ts @deprecated
/* eslint-disable-next-line deprecation/deprecation */
dashboardSearchLimit: configApi.getOptionalNumber(
'grafana.grafanaDashboardSearchLimit',
),
grafanaDashboardMaxPages: configApi.getOptionalNumber(
/* eslint-disable-next-line deprecation/deprecation */
dashboardMaxPages: configApi.getOptionalNumber(
'grafana.grafanaDashboardMaxPages',
),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ jest.mock('@backstage/plugin-catalog-react', () => ({
}));

describe('Entity card extensions', () => {
const mockGrafanaApi = {
const mockGrafanaApi: GrafanaApi = {
listDashboards: async () => [],
alertsForSelector: async () => [],
} as unknown as GrafanaApi;
isUnifiedAlerting: () => false,
};

it('should render the Alerts card', async () => {
renderInTestApp(
Expand Down
Loading
Loading