diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index c10c10065fd..af408bc8082 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -91,7 +91,7 @@ const unifiedRoutes: RouteDescriptor[] = [ { path: '/alerting', component: SafeDynamicImport( - () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/Home') + () => import(/* webpackChunkName: "AlertingHome" */ 'app/features/alerting/unified/home/Home') ), }, { diff --git a/public/app/features/alerting/unified/features.ts b/public/app/features/alerting/unified/features.ts index 76447761dea..4de5819d18d 100644 --- a/public/app/features/alerting/unified/features.ts +++ b/public/app/features/alerting/unified/features.ts @@ -6,6 +6,7 @@ export enum AlertingFeature { NotificationPoliciesV2MatchingInstances = 'notification-policies.v2.matching-instances', DetailsViewV2 = 'details-view.v2', ContactPointsV2 = 'contact-points.v2', + InsightsPage = 'insights-page', } const FEATURES: FeatureDescription[] = [ @@ -21,5 +22,9 @@ const FEATURES: FeatureDescription[] = [ name: AlertingFeature.DetailsViewV2, defaultValue: false, }, + { + name: AlertingFeature.InsightsPage, + defaultValue: false, + }, ]; export default FEATURES; diff --git a/public/app/features/alerting/unified/Home.tsx b/public/app/features/alerting/unified/home/GettingStarted.tsx similarity index 61% rename from public/app/features/alerting/unified/Home.tsx rename to public/app/features/alerting/unified/home/GettingStarted.tsx index 0dfb5e88db4..95d3591d652 100644 --- a/public/app/features/alerting/unified/Home.tsx +++ b/public/app/features/alerting/unified/home/GettingStarted.tsx @@ -6,74 +6,69 @@ import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { Icon, useStyles2, useTheme2 } from '@grafana/ui'; -import { AlertingPageWrapper } from './components/AlertingPageWrapper'; - -export default function Home() { +export default function GettingStarted({ showWelcomeHeader }: { showWelcomeHeader?: boolean }) { const theme = useTheme2(); const styles = useStyles2(getWelcomePageStyles); return ( - -
- - +
+ {showWelcomeHeader && } + +
+

How it works

+
    +
  • + Grafana alerting periodically queries data sources and evaluates the condition defined in the alert rule +
  • +
  • If the condition is breached, an alert instance fires
  • +
  • Firing instances are routed to notification policies based on matching labels
  • +
  • Notifications are sent out to the contact points specified in the notification policy
  • +
+
+ +
+ +

Get started

+ +
    +
  • + Create an alert rule by adding queries and expressions from multiple data sources. +
  • +
  • + Add labels to your alert rules to connect them to notification policies +
  • +
  • + Configure contact points to define where to send your notifications to. +
  • +
  • + Configure notification policies to route your alert instances to contact points. +
  • +
-

How it works

-
    -
  • - Grafana alerting periodically queries data sources and evaluates the condition defined in the alert rule -
  • -
  • If the condition is breached, an alert instance fires
  • -
  • Firing instances are routed to notification policies based on matching labels
  • -
  • Notifications are sent out to the contact points specified in the notification policy
  • -
+
- -
- -

Get started

- -
    -
  • - Create an alert rule by adding queries and expressions from multiple data sources. -
  • -
  • - Add labels to your alert rules{' '} - to connect them to notification policies -
  • -
  • - Configure contact points to define where to send your notifications to. -
  • -
  • - Configure notification policies to route your alert instances to contact points. -
  • -
-
- -
-
-
- - - -
- + +
+ + + +
); } @@ -105,7 +100,6 @@ const getWelcomePageStyles = (theme: GrafanaTheme2) => ({ `, videoBlock: css` grid-column: 3 / span 3; - grid-row: 3 / span 1; // Video required position: relative; @@ -132,7 +126,7 @@ const getWelcomePageStyles = (theme: GrafanaTheme2) => ({ `, }); -function WelcomeHeader({ className }: { className?: string }) { +export function WelcomeHeader({ className }: { className?: string }) { const styles = useStyles2(getWelcomeHeaderStyles); return ( diff --git a/public/app/features/alerting/unified/home/Home.tsx b/public/app/features/alerting/unified/home/Home.tsx new file mode 100644 index 00000000000..619a00c0f57 --- /dev/null +++ b/public/app/features/alerting/unified/home/Home.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { Enable, Disable } from 'react-enable'; + +import { Tab, TabContent, TabsBar } from '@grafana/ui'; + +import { AlertingPageWrapper } from '../components/AlertingPageWrapper'; +import { AlertingFeature } from '../features'; + +import GettingStarted, { WelcomeHeader } from './GettingStarted'; +import Insights from './Insights'; + +type HomeTabs = 'insights' | 'gettingStarted'; + +export default function Home() { + const [activeTab, setActiveTab] = useState('insights'); + + return ( + + + + + { + setActiveTab('insights'); + }} + /> + { + setActiveTab('gettingStarted'); + }} + /> + + + {activeTab === 'insights' && } + {activeTab === 'gettingStarted' && } + + + + + + + ); +} diff --git a/public/app/features/alerting/unified/home/Insights.tsx b/public/app/features/alerting/unified/home/Insights.tsx new file mode 100644 index 00000000000..abe83941c5f --- /dev/null +++ b/public/app/features/alerting/unified/home/Insights.tsx @@ -0,0 +1,160 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { + EmbeddedScene, + QueryVariable, + SceneFlexLayout, + SceneTimeRange, + SceneVariableSet, + VariableValueSelectors, +} from '@grafana/scenes'; +import { useStyles2, CollapsableSection } from '@grafana/ui'; + +import { getFiringAlertsScene } from '../insights/grafana/FiringAlertsPercentage'; +import { getFiringAlertsRateScene } from '../insights/grafana/FiringAlertsRate'; +import { getMostFiredInstancesScene } from '../insights/grafana/MostFiredInstancesTable'; +import { getAlertsByStateScene } from '../insights/mimir/AlertsByState'; +import { getInvalidConfigScene } from '../insights/mimir/InvalidConfig'; +import { getNotificationsScene } from '../insights/mimir/Notifications'; +import { getSilencesScene } from '../insights/mimir/Silences'; +import { getRuleGroupEvaluationDurationScene } from '../insights/mimir/perGroup/RuleGroupEvaluationDurationScene'; +import { getRuleGroupEvaluationsScene } from '../insights/mimir/perGroup/RuleGroupEvaluationsScene'; +import { getRuleGroupIntervalScene } from '../insights/mimir/perGroup/RuleGroupIntervalScene'; +import { getRulesPerGroupScene } from '../insights/mimir/perGroup/RulesPerGroupScene'; +import { getEvalSuccessVsFailuresScene } from '../insights/mimir/rules/EvalSuccessVsFailuresScene'; +import { getFiringCloudAlertsScene } from '../insights/mimir/rules/Firing'; +import { getInstancesByStateScene } from '../insights/mimir/rules/InstancesByState'; +import { getInstancesPercentageByStateScene } from '../insights/mimir/rules/InstancesPercentageByState'; +import { getMissedIterationsScene } from '../insights/mimir/rules/MissedIterationsScene'; +import { getMostFiredInstancesScene as getMostFiredCloudInstances } from '../insights/mimir/rules/MostFiredInstances'; +import { getPendingCloudAlertsScene } from '../insights/mimir/rules/Pending'; + +const ashDs = { + type: 'loki', + uid: 'grafanacloud-alert-state-history', +}; + +const cloudUsageDs = { + type: 'prometheus', + uid: 'grafanacloud-usage', +}; + +const grafanaCloudPromDs = { + type: 'prometheus', + uid: 'grafanacloud-prom', +}; + +const THIS_WEEK_TIME_RANGE = new SceneTimeRange({ from: 'now-1w', to: 'now' }); +const LAST_WEEK_TIME_RANGE = new SceneTimeRange({ from: 'now-2w', to: 'now-1w' }); + +function getGrafanaScenes() { + return new EmbeddedScene({ + body: new SceneFlexLayout({ + wrap: 'wrap', + children: [ + getMostFiredInstancesScene(THIS_WEEK_TIME_RANGE, ashDs, 'Top 10 firing instances this week'), + + getFiringAlertsRateScene(THIS_WEEK_TIME_RANGE, ashDs, 'Alerts firing per minute'), + + getFiringAlertsScene(THIS_WEEK_TIME_RANGE, ashDs, 'Firing alerts this week'), + + getFiringAlertsScene(LAST_WEEK_TIME_RANGE, ashDs, 'Firing alerts last week'), + ], + }), + }); +} + +function getCloudScenes() { + return new EmbeddedScene({ + body: new SceneFlexLayout({ + wrap: 'wrap', + children: [ + getAlertsByStateScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Alerts by State'), + getNotificationsScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Notifications'), + + getSilencesScene(LAST_WEEK_TIME_RANGE, cloudUsageDs, 'Silences'), + getInvalidConfigScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Invalid configuration'), + ], + }), + }); +} + +function getMimirManagedRulesScenes() { + return new EmbeddedScene({ + body: new SceneFlexLayout({ + wrap: 'wrap', + children: [ + getMostFiredCloudInstances(THIS_WEEK_TIME_RANGE, grafanaCloudPromDs, 'Top 10 firing instance this week'), + getFiringCloudAlertsScene(THIS_WEEK_TIME_RANGE, grafanaCloudPromDs, 'Firing'), + getPendingCloudAlertsScene(THIS_WEEK_TIME_RANGE, grafanaCloudPromDs, 'Pending'), + + getInstancesByStateScene(THIS_WEEK_TIME_RANGE, grafanaCloudPromDs, 'Count of alert instances by state'), + getInstancesPercentageByStateScene(THIS_WEEK_TIME_RANGE, grafanaCloudPromDs, '% of Alert Instances by State'), + + getEvalSuccessVsFailuresScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Evaluation Success vs Failures'), + getMissedIterationsScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Iterations Missed'), + ], + }), + }); +} + +function getMimirManagedRulesPerGroupScenes() { + const ruleGroupHandler = new QueryVariable({ + label: 'Rule Group', + name: 'rule_group', + datasource: cloudUsageDs, + query: 'label_values(grafanacloud_instance_rule_group_rules,rule_group)', + }); + + return new EmbeddedScene({ + body: new SceneFlexLayout({ + wrap: 'wrap', + children: [ + getRuleGroupEvaluationsScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Rule Group Evaluation'), + getRuleGroupIntervalScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Rule Group Interval'), + getRuleGroupEvaluationDurationScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Rule Group Evaluation Duration'), + getRulesPerGroupScene(THIS_WEEK_TIME_RANGE, cloudUsageDs, 'Rules per Group'), + ], + }), + $variables: new SceneVariableSet({ + variables: [ruleGroupHandler], + }), + controls: [new VariableValueSelectors({})], + }); +} + +export default function Insights() { + const styles = useStyles2(getStyles); + const grafanaScenes = getGrafanaScenes(); + const cloudScenes = getCloudScenes(); + const mimirRulesScenes = getMimirManagedRulesScenes(); + const mimirRulesPerGroupScenes = getMimirManagedRulesPerGroupScenes(); + + return ( +
+ + + + + + + + + + + + + + + +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + container: css({ + padding: '10px 0 10px 0', + }), +}); diff --git a/public/app/features/alerting/unified/insights/grafana/FiringAlertsPercentage.tsx b/public/app/features/alerting/unified/insights/grafana/FiringAlertsPercentage.tsx new file mode 100644 index 00000000000..a59a0d96af4 --- /dev/null +++ b/public/app/features/alerting/unified/insights/grafana/FiringAlertsPercentage.tsx @@ -0,0 +1,68 @@ +import { PanelBuilders, SceneDataTransformer, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +const TOTALS = 'sum(count_over_time({from="state-history"} | json [1w]))'; +const TOTALS_FIRING = 'sum(count_over_time({from="state-history"} | json | current="Alerting"[1w]))'; + +export function getFiringAlertsScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + instant: true, + expr: TOTALS_FIRING, + }, + { + refId: 'B', + instant: true, + expr: TOTALS, + }, + ], + $timeRange: timeRange, + }); + + const transformation = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'calculateField', + options: { + mode: 'binary', + reduce: { + reducer: 'mean', + }, + replaceFields: false, + binary: { + left: 'Value #A', + reducer: 'sum', + operator: '*', + right: '100', + }, + }, + }, + { + id: 'calculateField', + options: { + mode: 'binary', + reduce: { + reducer: 'sum', + }, + binary: { + left: 'Value #A * 100', + reducer: 'sum', + operator: '/', + right: 'Value #B', + }, + replaceFields: true, + }, + }, + ], + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.stat().setTitle(panelTitle).setData(transformation).setUnit('percent').build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/grafana/FiringAlertsRate.tsx b/public/app/features/alerting/unified/insights/grafana/FiringAlertsRate.tsx new file mode 100644 index 00000000000..b3c6dc58780 --- /dev/null +++ b/public/app/features/alerting/unified/insights/grafana/FiringAlertsRate.tsx @@ -0,0 +1,32 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const RATE_FIRING = 'sum(count_over_time({from="state-history"} | json | current="Alerting"[5m]))'; + +export function getFiringAlertsRateScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: RATE_FIRING, + range: true, + legendFormat: 'Number of fires', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .setCustomFieldConfig('fillOpacity', 10) + .setCustomFieldConfig('spanNulls', true) + .setCustomFieldConfig('pointSize', 5) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/grafana/MostFiredInstancesTable.tsx b/public/app/features/alerting/unified/insights/grafana/MostFiredInstancesTable.tsx new file mode 100644 index 00000000000..86759ce47d8 --- /dev/null +++ b/public/app/features/alerting/unified/insights/grafana/MostFiredInstancesTable.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Observable, map } from 'rxjs'; + +import { DataFrame, Field } from '@grafana/data'; +import { + CustomTransformOperator, + PanelBuilders, + SceneDataTransformer, + SceneFlexItem, + SceneQueryRunner, + SceneTimeRange, +} from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; +import { Icon, Link } from '@grafana/ui'; + +import { createUrl } from '../../utils/url'; + +const TOP_FIRING_INSTANCES = + 'topk(10, sum by(labels_alertname, ruleUID) (count_over_time({from="state-history"} | json | current = `Alerting` [1w])))'; + +export function getMostFiredInstancesScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: TOP_FIRING_INSTANCES, + instant: true, + }, + ], + + $timeRange: timeRange, + }); + + const createRuleLink = (field: Field, frame: DataFrame) => { + return { + ...field, + values: field.values.map((value, index) => { + const ruleUIDs = frame.fields.find((field) => field.name === 'ruleUID'); + const ruleUID = ruleUIDs?.values[index]; + return ( + + {value} + + ); + }), + }; + }; + + const ruleLinkTransformation: CustomTransformOperator = () => (source: Observable) => { + return source.pipe( + map((data: DataFrame[]) => { + return data.map((frame: DataFrame) => { + return { + ...frame, + fields: frame.fields.map((field) => { + //Transforming the column `labels_alertname` to show a link to the rule view page next to the alert name + if (field.name === 'labels_alertname') { + return createRuleLink(field, frame); + } + return field; + }), + }; + }); + }) + ); + }; + + const transformation = new SceneDataTransformer({ + $data: query, + transformations: [ + ruleLinkTransformation, + { + id: 'sortBy', + options: { + fields: {}, + sort: [ + { + field: 'Value #A', + desc: true, + }, + ], + }, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + ruleUID: true, + }, + indexByName: { + labels_alertname: 0, + 'Value #A': 1, + }, + renameByName: { + labels_alertname: 'Alert Name', + 'Value #A': 'Fires this week', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.table().setTitle(panelTitle).setData(transformation).build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/grafana/MostFiredRulesTable.tsx b/public/app/features/alerting/unified/insights/grafana/MostFiredRulesTable.tsx new file mode 100644 index 00000000000..a4c5a8d2b1f --- /dev/null +++ b/public/app/features/alerting/unified/insights/grafana/MostFiredRulesTable.tsx @@ -0,0 +1,61 @@ +import { PanelBuilders, SceneDataTransformer, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +const TOP_5_FIRING_RULES = + 'topk(5, sum by(group, labels_grafana_folder) (count_over_time({from="state-history"} | json | current = `Alerting` [1w])))'; + +export function getMostFiredRulesScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: TOP_5_FIRING_RULES, + instant: true, + }, + ], + $timeRange: timeRange, + }); + + const transformation = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'sortBy', + options: { + fields: {}, + sort: [ + { + field: 'Value #A', + desc: true, + }, + ], + }, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + }, + indexByName: { + group: 0, + labels_grafana_folder: 1, + 'Value #A': 2, + }, + renameByName: { + group: 'Group', + labels_grafana_folder: 'Folder', + 'Value #A': 'Fires this week', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.table().setTitle(panelTitle).setData(transformation).build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/grafana/MostFiringLabels.tsx b/public/app/features/alerting/unified/insights/grafana/MostFiringLabels.tsx new file mode 100644 index 00000000000..eb3b084ca7b --- /dev/null +++ b/public/app/features/alerting/unified/insights/grafana/MostFiringLabels.tsx @@ -0,0 +1,56 @@ +import { Observable } from 'rxjs'; + +import { DataQueryRequest, DataQueryResponse, DataQueryResponseData, TestDataSourceResponse } from '@grafana/data'; +import { getBackendSrv } from '@grafana/runtime'; +import { + PanelBuilders, + RuntimeDataSource, + SceneFlexItem, + SceneQueryRunner, + SceneTimeRange, + sceneUtils, +} from '@grafana/scenes'; +import { DataQuery, DataSourceRef } from '@grafana/schema'; +import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; + +export const getLabelsInfo = (from: number, to: number): Observable => { + return getBackendSrv().fetch({ + url: `/api/v1/rules/history`, + params: { from, to, limit: 100 }, + method: 'GET', + }); +}; + +class LokiAPIDatasource extends RuntimeDataSource { + private timeRange: SceneTimeRange; + + constructor(pluginId: string, uid: string, timeRange: SceneTimeRange) { + super(pluginId, uid); + this.timeRange = timeRange; + } + + query(request: DataQueryRequest): Promise | Observable { + const timeRange = getTimeRange({ from: this.timeRange.state.from, to: this.timeRange.state.to }); + return getLabelsInfo(timeRange.from.unix(), timeRange.to.unix()); + } + + testDatasource(): Promise { + return Promise.resolve({ status: 'success', message: 'Data source is working', title: 'Success' }); + } +} + +export function getMostFiredLabelsScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + sceneUtils.registerRuntimeDataSource({ dataSource: new LokiAPIDatasource('loki-api-ds', 'LOKI-API', timeRange) }); + + const query = new SceneQueryRunner({ + datasource: { uid: 'LOKI-API', type: 'loki-api-ds' }, + queries: [{ refId: 'A', expr: 'vector(1)' }], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 8px)', + height: 300, + body: PanelBuilders.table().setTitle(panelTitle).setData(query).build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx b/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx new file mode 100644 index 00000000000..442f32df450 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/AlertsByState.tsx @@ -0,0 +1,29 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY = 'sum by (state) (grafanacloud_instance_alertmanager_alerts)'; + +export function getAlertsByStateScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY, + range: true, + legendFormat: '{{state}}', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx b/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx new file mode 100644 index 00000000000..935bf1408dc --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/InvalidConfig.tsx @@ -0,0 +1,30 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = 'sum by (cluster)(grafanacloud_instance_alertmanager_invalid_config)'; + +export function getInvalidConfigScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: '{{cluster}}', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .setUnit('bool_yes_no') + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/Notifications.tsx b/public/app/features/alerting/unified/insights/mimir/Notifications.tsx new file mode 100644 index 00000000000..666e7133ada --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/Notifications.tsx @@ -0,0 +1,37 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = + 'sum by(cluster)(grafanacloud_instance_alertmanager_notifications_per_second) - sum by (cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second)'; +const QUERY_B = 'sum by(cluster)(grafanacloud_instance_alertmanager_notifications_failed_per_second)'; + +export function getNotificationsScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: '{{cluster}} - success', + }, + { + refId: 'B', + expr: QUERY_B, + range: true, + legendFormat: '{{cluster}} - failed', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/Silences.tsx b/public/app/features/alerting/unified/insights/mimir/Silences.tsx new file mode 100644 index 00000000000..4972a021012 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/Silences.tsx @@ -0,0 +1,29 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = 'sum by (state) (grafanacloud_instance_alertmanager_silences)'; + +export function getSilencesScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: '{{state}}', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx new file mode 100644 index 00000000000..3830a1de2d0 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationDurationScene.tsx @@ -0,0 +1,34 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = `grafanacloud_instance_rule_group_last_duration_seconds{rule_group="$rule_group"}`; + +export function getRuleGroupEvaluationDurationScene( + timeRange: SceneTimeRange, + datasource: DataSourceRef, + panelTitle: string +) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: '{{rule_group}}', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .setUnit('s') + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx new file mode 100644 index 00000000000..c2eb7a13798 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupEvaluationsScene.tsx @@ -0,0 +1,36 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = `grafanacloud_instance_rule_evaluations_total:rate5m{rule_group="$rule_group"} - grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group"}`; +const QUERY_B = `grafanacloud_instance_rule_evaluation_failures_total:rate5m{rule_group=~"$rule_group"}`; + +export function getRuleGroupEvaluationsScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: 'success', + }, + { + refId: 'B', + expr: QUERY_B, + range: true, + legendFormat: 'failed', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx new file mode 100644 index 00000000000..6fd3fc9cf8f --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RuleGroupIntervalScene.tsx @@ -0,0 +1,30 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = `grafanacloud_instance_rule_group_interval_seconds{rule_group="$rule_group"}`; + +export function getRuleGroupIntervalScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: 'interval', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .setUnit('s') + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx b/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx new file mode 100644 index 00000000000..44ffc0390bc --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/perGroup/RulesPerGroupScene.tsx @@ -0,0 +1,30 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = `sum(grafanacloud_instance_rule_group_rules{rule_group="$rule_group"})`; + +export function getRulesPerGroupScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: 'number of rules', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .setUnit('none') + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx b/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx new file mode 100644 index 00000000000..4fce85c0332 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/rules/EvalSuccessVsFailuresScene.tsx @@ -0,0 +1,42 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = + 'sum(grafanacloud_instance_rule_evaluations_total:rate5m) - sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m)'; + +const QUERY_B = 'sum(grafanacloud_instance_rule_evaluation_failures_total:rate5m)'; + +export function getEvalSuccessVsFailuresScene( + timeRange: SceneTimeRange, + datasource: DataSourceRef, + panelTitle: string +) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: 'success', + }, + { + refId: 'B', + expr: QUERY_B, + range: true, + legendFormat: 'failed', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/rules/Firing.tsx b/public/app/features/alerting/unified/insights/mimir/rules/Firing.tsx new file mode 100644 index 00000000000..5ade01a1580 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/rules/Firing.tsx @@ -0,0 +1,41 @@ +import { ThresholdsMode } from '@grafana/data'; +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +const QUERY = 'sum by (alertstate) (ALERTS{alertstate="firing"})'; + +export function getFiringCloudAlertsScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + instant: true, + expr: QUERY, + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(25% - 4px)', + height: 300, + body: PanelBuilders.stat() + .setTitle(panelTitle) + .setData(query) + .setThresholds({ + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'red', + value: 0, + }, + { + color: 'red', + value: 80, + }, + ], + }) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/rules/InstancesByState.tsx b/public/app/features/alerting/unified/insights/mimir/rules/InstancesByState.tsx new file mode 100644 index 00000000000..88ab15571a2 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/rules/InstancesByState.tsx @@ -0,0 +1,29 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY = 'sum by (alertstate) (ALERTS)'; + +export function getInstancesByStateScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY, + range: true, + legendFormat: '{{state}}', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/rules/InstancesPercentageByState.tsx b/public/app/features/alerting/unified/insights/mimir/rules/InstancesPercentageByState.tsx new file mode 100644 index 00000000000..298aa62f9a1 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/rules/InstancesPercentageByState.tsx @@ -0,0 +1,50 @@ +import { ThresholdsMode } from '@grafana/data'; +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY = 'sum by (alertstate) (ALERTS) / ignoring(alertstate) group_left sum(ALERTS)'; + +export function getInstancesPercentageByStateScene( + timeRange: SceneTimeRange, + datasource: DataSourceRef, + panelTitle: string +) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY, + range: true, + legendFormat: '{{alertstate}}', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .setCustomFieldConfig('fillOpacity', 45) + .setUnit('percentunit') + .setMax(1) + .setThresholds({ + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: 0, + }, + { + color: 'red', + value: 80, + }, + ], + }) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx b/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx new file mode 100644 index 00000000000..ae2f8dc102a --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/rules/MissedIterationsScene.tsx @@ -0,0 +1,29 @@ +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef, GraphDrawStyle } from '@grafana/schema'; + +const QUERY_A = 'sum(grafanacloud_instance_rule_group_iterations_missed_total:rate5m)'; + +export function getMissedIterationsScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: QUERY_A, + range: true, + legendFormat: 'missed', + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(50% - 4px)', + height: 300, + body: PanelBuilders.timeseries() + .setTitle(panelTitle) + .setData(query) + .setCustomFieldConfig('drawStyle', GraphDrawStyle.Line) + .build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/rules/MostFiredInstances.tsx b/public/app/features/alerting/unified/insights/mimir/rules/MostFiredInstances.tsx new file mode 100644 index 00000000000..0f56724dfee --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/rules/MostFiredInstances.tsx @@ -0,0 +1,46 @@ +import { PanelBuilders, SceneDataTransformer, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +const TOP_5_FIRING_INSTANCES = 'topk(10, sum by (alertname) (ALERTS))'; + +export function getMostFiredInstancesScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + expr: TOP_5_FIRING_INSTANCES, + range: true, + }, + ], + + $timeRange: timeRange, + }); + + const transformation = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'timeSeriesTable', + options: {}, + }, + { + id: 'organize', + options: { + excludeByName: {}, + indexByName: {}, + renameByName: { + Trend: '', + alertname: 'Alert Rule Name', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + width: 'calc(50% - 8px)', + height: 300, + body: PanelBuilders.table().setTitle(panelTitle).setData(transformation).build(), + }); +} diff --git a/public/app/features/alerting/unified/insights/mimir/rules/Pending.tsx b/public/app/features/alerting/unified/insights/mimir/rules/Pending.tsx new file mode 100644 index 00000000000..42ea05a7903 --- /dev/null +++ b/public/app/features/alerting/unified/insights/mimir/rules/Pending.tsx @@ -0,0 +1,41 @@ +import { ThresholdsMode } from '@grafana/data'; +import { PanelBuilders, SceneFlexItem, SceneQueryRunner, SceneTimeRange } from '@grafana/scenes'; +import { DataSourceRef } from '@grafana/schema'; + +const QUERY = 'sum by (alertstate) (ALERTS{alertstate="pending"})'; + +export function getPendingCloudAlertsScene(timeRange: SceneTimeRange, datasource: DataSourceRef, panelTitle: string) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + refId: 'A', + instant: true, + expr: QUERY, + }, + ], + $timeRange: timeRange, + }); + + return new SceneFlexItem({ + width: 'calc(25% - 4px)', + height: 300, + body: PanelBuilders.stat() + .setTitle(panelTitle) + .setData(query) + .setThresholds({ + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'yellow', + value: 0, + }, + { + color: 'red', + value: 80, + }, + ], + }) + .build(), + }); +}