mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
DashboardScene: Alert states data layer (#77945)
* Add AlertStates data topic * DashboardScene: Alert states data layer * TMP package json * Remove duplicated function * Use latest scenes canry * Use latest scenes and add transformation test
This commit is contained in:
parent
ea12eecac5
commit
a3ae9d418d
@ -2873,7 +2873,9 @@ exports[`better eslint`] = {
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
@ -254,7 +254,7 @@
|
||||
"@grafana/lezer-traceql": "0.0.10",
|
||||
"@grafana/monaco-logql": "^0.0.7",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "^1.21.1",
|
||||
"@grafana/scenes": "^1.22.0",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"@kusto/monaco-kusto": "^7.4.0",
|
||||
|
@ -17,6 +17,7 @@ export interface DataSourceRef extends SchemaDataSourceRef {}
|
||||
*/
|
||||
export enum DataTopic {
|
||||
Annotations = 'annotations',
|
||||
AlertStates = 'alertStates',
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,227 @@
|
||||
import { from, map, Unsubscribable, Observable } from 'rxjs';
|
||||
|
||||
import { AlertState, AlertStateInfo, DataTopic, LoadingState, toDataFrame } from '@grafana/data';
|
||||
import { config, getBackendSrv } from '@grafana/runtime';
|
||||
import {
|
||||
SceneDataLayerBase,
|
||||
SceneDataLayerProvider,
|
||||
SceneDataLayerProviderState,
|
||||
sceneGraph,
|
||||
SceneTimeRangeLike,
|
||||
} from '@grafana/scenes';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import { dispatch } from 'app/store/store';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromAlertingRuleState, PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
interface AlertStatesDataLayerState extends SceneDataLayerProviderState {}
|
||||
|
||||
export class AlertStatesDataLayer
|
||||
extends SceneDataLayerBase<AlertStatesDataLayerState>
|
||||
implements SceneDataLayerProvider
|
||||
{
|
||||
private hasAlertRules = true;
|
||||
private _timeRangeSub: Unsubscribable | undefined;
|
||||
public topic = DataTopic.AlertStates;
|
||||
|
||||
public constructor(initialState: AlertStatesDataLayerState) {
|
||||
super({
|
||||
isEnabled: true,
|
||||
...initialState,
|
||||
isHidden: true,
|
||||
});
|
||||
}
|
||||
|
||||
public onEnable(): void {
|
||||
const timeRange = sceneGraph.getTimeRange(this);
|
||||
|
||||
this._timeRangeSub = timeRange.subscribeToState(() => {
|
||||
this.runWithTimeRange(timeRange);
|
||||
});
|
||||
}
|
||||
|
||||
public onDisable(): void {
|
||||
this._timeRangeSub?.unsubscribe();
|
||||
}
|
||||
|
||||
public runLayer() {
|
||||
const timeRange = sceneGraph.getTimeRange(this);
|
||||
this.runWithTimeRange(timeRange);
|
||||
}
|
||||
|
||||
private async runWithTimeRange(timeRange: SceneTimeRangeLike) {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
const { uid, id } = dashboard.state;
|
||||
|
||||
if (this.querySub) {
|
||||
this.querySub.unsubscribe();
|
||||
}
|
||||
|
||||
if (!this.canWork(timeRange)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let alerStatesExecution: Observable<AlertStateInfo[]> | undefined;
|
||||
|
||||
if (this.isUsingLegacyAlerting()) {
|
||||
alerStatesExecution = from(
|
||||
getBackendSrv().get(
|
||||
'/api/alerts/states-for-dashboard',
|
||||
{
|
||||
dashboardId: id,
|
||||
},
|
||||
`dashboard-query-runner-alert-states-${id}`
|
||||
)
|
||||
).pipe(map((alertStates) => alertStates));
|
||||
} else {
|
||||
alerStatesExecution = from(
|
||||
getBackendSrv().get(
|
||||
'/api/prometheus/grafana/api/v1/rules',
|
||||
{
|
||||
dashboard_uid: uid!,
|
||||
},
|
||||
`dashboard-query-runner-unified-alert-states-${id}`
|
||||
)
|
||||
).pipe(
|
||||
map((result: PromRulesResponse) => {
|
||||
if (result.status === 'success') {
|
||||
this.hasAlertRules = false;
|
||||
const panelIdToAlertState: Record<number, AlertStateInfo> = {};
|
||||
|
||||
result.data.groups.forEach((group) =>
|
||||
group.rules.forEach((rule) => {
|
||||
if (isAlertingRule(rule) && rule.annotations && rule.annotations[Annotation.panelID]) {
|
||||
this.hasAlertRules = true;
|
||||
const panelId = Number(rule.annotations[Annotation.panelID]);
|
||||
const state = promAlertStateToAlertState(rule.state);
|
||||
|
||||
// there can be multiple alerts per panel, so we make sure we get the most severe state:
|
||||
// alerting > pending > ok
|
||||
if (!panelIdToAlertState[panelId]) {
|
||||
panelIdToAlertState[panelId] = {
|
||||
state,
|
||||
id: Object.keys(panelIdToAlertState).length,
|
||||
panelId,
|
||||
dashboardId: id!,
|
||||
};
|
||||
} else if (
|
||||
state === AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Alerting;
|
||||
} else if (
|
||||
state === AlertState.Pending &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Alerting &&
|
||||
panelIdToAlertState[panelId].state !== AlertState.Pending
|
||||
) {
|
||||
panelIdToAlertState[panelId].state = AlertState.Pending;
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
return Object.values(panelIdToAlertState);
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected alert rules response.`);
|
||||
})
|
||||
);
|
||||
}
|
||||
this.querySub = alerStatesExecution.subscribe({
|
||||
next: (stateUpdate) => {
|
||||
this.publishResults(
|
||||
{
|
||||
state: LoadingState.Done,
|
||||
series: [toDataFrame(stateUpdate)],
|
||||
timeRange: timeRange.state.value,
|
||||
},
|
||||
DataTopic.AlertStates
|
||||
);
|
||||
},
|
||||
error: (err) => {
|
||||
this.handleError(err);
|
||||
this.publishResults(
|
||||
{
|
||||
state: LoadingState.Error,
|
||||
series: [],
|
||||
errors: [
|
||||
{
|
||||
message: getMessageFromError(err),
|
||||
},
|
||||
],
|
||||
timeRange: timeRange.state.value,
|
||||
},
|
||||
DataTopic.AlertStates
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private canWork(timeRange: SceneTimeRangeLike): boolean {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
const { uid, id } = dashboard.state;
|
||||
|
||||
if (this.isUsingLegacyAlerting()) {
|
||||
if (!id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timeRange.state.value.raw.to !== 'now') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (!uid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot fetch rules while on a public dashboard since it's unauthenticated
|
||||
if (config.publicDashboardAccessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timeRange.state.value.raw.to !== 'now') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.hasAlertRules === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasRuleReadPermission =
|
||||
contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) &&
|
||||
contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead);
|
||||
|
||||
if (!hasRuleReadPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private isUsingLegacyAlerting(): boolean {
|
||||
return !config.unifiedAlertingEnabled;
|
||||
}
|
||||
|
||||
private handleError = (err: unknown) => {
|
||||
const notification = createErrorNotification('AlertStatesDataLayer', getMessageFromError(err));
|
||||
dispatch(notifyApp(notification));
|
||||
};
|
||||
}
|
||||
|
||||
export function promAlertStateToAlertState(state: PromAlertingRuleState): AlertState {
|
||||
if (state === PromAlertingRuleState.Firing) {
|
||||
return AlertState.Alerting;
|
||||
} else if (state === PromAlertingRuleState.Pending) {
|
||||
return AlertState.Pending;
|
||||
}
|
||||
return AlertState.OK;
|
||||
}
|
@ -28,6 +28,7 @@ import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { createPanelSaveModel } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem';
|
||||
import { PanelTimeRange } from '../scene/PanelTimeRange';
|
||||
@ -743,6 +744,34 @@ describe('transformSaveModelToScene', () => {
|
||||
expect(dataLayers.state.layers[3].state.isHidden).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Alerting data layer', () => {
|
||||
it('Should add alert states data layer if unified alerting enabled', () => {
|
||||
config.unifiedAlertingEnabled = true;
|
||||
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
||||
|
||||
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
|
||||
expect(scene.state.controls![2]).toBeInstanceOf(SceneDataLayerControls);
|
||||
|
||||
const dataLayers = scene.state.$data as SceneDataLayers;
|
||||
expect(dataLayers.state.layers).toHaveLength(5);
|
||||
expect(dataLayers.state.layers[4].state.name).toBe('Alert States');
|
||||
});
|
||||
|
||||
it('Should add alert states data layer if any panel has a legacy alert defined', () => {
|
||||
config.unifiedAlertingEnabled = false;
|
||||
const dashboard = { ...dashboard_to_load1 } as unknown as DashboardDataDTO;
|
||||
dashboard.panels![0].alert = {};
|
||||
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
||||
|
||||
expect(scene.state.$data).toBeInstanceOf(SceneDataLayers);
|
||||
expect(scene.state.controls![2]).toBeInstanceOf(SceneDataLayerControls);
|
||||
|
||||
const dataLayers = scene.state.$data as SceneDataLayers;
|
||||
expect(dataLayers.state.layers).toHaveLength(5);
|
||||
expect(dataLayers.state.layers[4].state.name).toBe('Alert States');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildGridItemForTest(saveModel: Partial<Panel>): { gridItem: SceneGridItem; vizPanel: VizPanel } {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AdHocVariableModel, TypedVariableModel, VariableModel } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
VizPanel,
|
||||
SceneTimePicker,
|
||||
@ -29,6 +30,7 @@ import {
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { DashboardDTO } from 'app/types';
|
||||
|
||||
import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer';
|
||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
@ -187,6 +189,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
||||
layers = oldModel.annotations?.list.map((a) => {
|
||||
// Each annotation query is an individual data layer
|
||||
return new DashboardAnnotationsDataLayer({
|
||||
key: `annnotations-${a.name}`,
|
||||
query: a,
|
||||
name: a.name,
|
||||
isEnabled: Boolean(a.enable),
|
||||
@ -195,6 +198,22 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
||||
});
|
||||
}
|
||||
|
||||
let shouldUseAlertStatesLayer = config.unifiedAlertingEnabled;
|
||||
if (!shouldUseAlertStatesLayer) {
|
||||
if (oldModel.panels.find((panel) => Boolean(panel.alert))) {
|
||||
shouldUseAlertStatesLayer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldUseAlertStatesLayer) {
|
||||
layers.push(
|
||||
new AlertStatesDataLayer({
|
||||
key: 'alert-states',
|
||||
name: 'Alert States',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
let controls: SceneObject[] = [
|
||||
new VariableValueSelectors({}),
|
||||
...filtersSets,
|
||||
|
@ -6,8 +6,9 @@ import { config, getBackendSrv } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { Annotation } from 'app/features/alerting/unified/utils/constants';
|
||||
import { isAlertingRule } from 'app/features/alerting/unified/utils/rules';
|
||||
import { promAlertStateToAlertState } from 'app/features/dashboard-scene/scene/AlertStatesDataLayer';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { PromAlertingRuleState, PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
import { PromRulesResponse } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { DashboardQueryRunnerOptions, DashboardQueryRunnerWorker, DashboardQueryRunnerWorkerResult } from './types';
|
||||
import { emptyResult, handleDashboardQueryRunnerWorkerError } from './utils';
|
||||
@ -105,12 +106,3 @@ export class UnifiedAlertStatesWorker implements DashboardQueryRunnerWorker {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function promAlertStateToAlertState(state: PromAlertingRuleState): AlertState {
|
||||
if (state === PromAlertingRuleState.Firing) {
|
||||
return AlertState.Alerting;
|
||||
} else if (state === PromAlertingRuleState.Pending) {
|
||||
return AlertState.Pending;
|
||||
}
|
||||
return AlertState.OK;
|
||||
}
|
||||
|
10
yarn.lock
10
yarn.lock
@ -3320,9 +3320,9 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes@npm:^1.21.1":
|
||||
version: 1.21.1
|
||||
resolution: "@grafana/scenes@npm:1.21.1"
|
||||
"@grafana/scenes@npm:^1.22.0":
|
||||
version: 1.22.0
|
||||
resolution: "@grafana/scenes@npm:1.22.0"
|
||||
dependencies:
|
||||
"@grafana/e2e-selectors": "npm:10.0.2"
|
||||
react-grid-layout: "npm:1.3.4"
|
||||
@ -3334,7 +3334,7 @@ __metadata:
|
||||
"@grafana/runtime": 10.0.3
|
||||
"@grafana/schema": 10.0.3
|
||||
"@grafana/ui": 10.0.3
|
||||
checksum: f9621b0edcc5a9da2cfeac679bf9ea8d2ae6fc64c635f5bf8ee90c47cdf7a8e8799b4ef4d41b1fdee056371e9f5bfc73f5e5b2dc23852a1b05963748559fd6e9
|
||||
checksum: 6067bccec76de3f5aeab15c943095cf11e407b5734ddab4dc52ed40e469f65b096d69b7b0f46e5abe0a51c6691651c8ca8609177b95a32a2419239068a337952
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -17325,7 +17325,7 @@ __metadata:
|
||||
"@grafana/lezer-traceql": "npm:0.0.10"
|
||||
"@grafana/monaco-logql": "npm:^0.0.7"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:^1.21.1"
|
||||
"@grafana/scenes": "npm:^1.22.0"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/tsconfig": "npm:^1.3.0-rc1"
|
||||
"@grafana/ui": "workspace:*"
|
||||
|
Loading…
Reference in New Issue
Block a user