mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: bootstrap silences page (#32810)
This commit is contained in:
parent
c9e5088e8b
commit
e6a98ce1e4
@ -196,6 +196,9 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
|||||||
alertChildNavs := []*dtos.NavLink{
|
alertChildNavs := []*dtos.NavLink{
|
||||||
{Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul"},
|
{Text: "Alert rules", Id: "alert-list", Url: hs.Cfg.AppSubURL + "/alerting/list", Icon: "list-ul"},
|
||||||
}
|
}
|
||||||
|
if hs.Cfg.IsNgAlertEnabled() {
|
||||||
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Silences", Id: "silences", Url: hs.Cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"})
|
||||||
|
}
|
||||||
if c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsNgAlertEnabled() {
|
if c.OrgRole == models.ROLE_ADMIN && hs.Cfg.IsNgAlertEnabled() {
|
||||||
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
|
alertChildNavs = append(alertChildNavs, &dtos.NavLink{Text: "Routes", Id: "am-routes", Url: hs.Cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"})
|
||||||
}
|
}
|
||||||
|
@ -31,10 +31,13 @@ export const Page: PageType = ({ navModel, children, className, contentWidth, ..
|
|||||||
}, [navModel]);
|
}, [navModel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...otherProps} className={cx(styles.wrapper, className)}>
|
<div
|
||||||
|
{...otherProps}
|
||||||
|
className={cx(styles.wrapper, className, contentWidth ? styles.contentWidth(contentWidth) : undefined)}
|
||||||
|
>
|
||||||
<CustomScrollbar autoHeightMin={'100%'}>
|
<CustomScrollbar autoHeightMin={'100%'}>
|
||||||
<div className="page-scrollbar-content">
|
<div className="page-scrollbar-content">
|
||||||
<PageHeader model={navModel} contentWidth={contentWidth} />
|
<PageHeader model={navModel} />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
@ -56,4 +59,9 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background: ${theme.colors.bg1};
|
background: ${theme.colors.bg1};
|
||||||
`,
|
`,
|
||||||
|
contentWidth: (size: keyof GrafanaTheme['breakpoints']) => css`
|
||||||
|
.page-container {
|
||||||
|
max-width: ${theme.breakpoints[size]};
|
||||||
|
}
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { Tab, TabsBar, Icon, IconName, useStyles } from '@grafana/ui';
|
import { Tab, TabsBar, Icon, IconName, useStyles } from '@grafana/ui';
|
||||||
import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme } from '@grafana/data';
|
import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme } from '@grafana/data';
|
||||||
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
model: NavModel;
|
model: NavModel;
|
||||||
contentWidth?: keyof GrafanaTheme['breakpoints'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => {
|
const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => {
|
||||||
@ -72,7 +71,7 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader: FC<Props> = ({ model, contentWidth }) => {
|
export const PageHeader: FC<Props> = ({ model }) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
@ -84,7 +83,7 @@ export const PageHeader: FC<Props> = ({ model, contentWidth }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.headerCanvas}>
|
<div className={styles.headerCanvas}>
|
||||||
<div className={cx('page-container', contentWidth ? styles.contentWidth(contentWidth) : undefined)}>
|
<div className="page-container">
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
{renderHeaderTitle(main)}
|
{renderHeaderTitle(main)}
|
||||||
{children && children.length && <Navigation>{children}</Navigation>}
|
{children && children.length && <Navigation>{children}</Navigation>}
|
||||||
@ -143,9 +142,6 @@ const getStyles = (theme: GrafanaTheme) => ({
|
|||||||
background: ${theme.colors.bg2};
|
background: ${theme.colors.bg2};
|
||||||
border-bottom: 1px solid ${theme.colors.border1};
|
border-bottom: 1px solid ${theme.colors.border1};
|
||||||
`,
|
`,
|
||||||
contentWidth: (size: keyof GrafanaTheme['breakpoints']) => css`
|
|
||||||
max-width: ${theme.breakpoints[size]};
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default PageHeader;
|
export default PageHeader;
|
||||||
|
@ -69,6 +69,8 @@ export const RuleList: FC = () => {
|
|||||||
const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
||||||
const grafanaRulerError = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
const grafanaRulerError = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
||||||
|
|
||||||
|
const showNewAlertSplash = dispatched && !loading && !haveResults;
|
||||||
|
|
||||||
const combinedNamespaces = useCombinedRuleNamespaces();
|
const combinedNamespaces = useCombinedRuleNamespaces();
|
||||||
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
||||||
const sorted = combinedNamespaces
|
const sorted = combinedNamespaces
|
||||||
@ -116,13 +118,15 @@ export const RuleList: FC = () => {
|
|||||||
))}
|
))}
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
)}
|
)}
|
||||||
<div className={styles.buttonsContainer}>
|
{!showNewAlertSplash && (
|
||||||
<div />
|
<div className={styles.buttonsContainer}>
|
||||||
<a href="/alerting/new">
|
<div />
|
||||||
<Button icon="plus">New alert rule</Button>
|
<a href="/alerting/new">
|
||||||
</a>
|
<Button icon="plus">New alert rule</Button>
|
||||||
</div>
|
</a>
|
||||||
{dispatched && !loading && !haveResults && <NoRulesSplash />}
|
</div>
|
||||||
|
)}
|
||||||
|
{showNewAlertSplash && <NoRulesSplash />}
|
||||||
{haveResults && <ThresholdRules namespaces={thresholdNamespaces} />}
|
{haveResults && <ThresholdRules namespaces={thresholdNamespaces} />}
|
||||||
{haveResults && <SystemOrApplicationRules namespaces={systemNamespaces} />}
|
{haveResults && <SystemOrApplicationRules namespaces={systemNamespaces} />}
|
||||||
</AlertingPageWrapper>
|
</AlertingPageWrapper>
|
||||||
|
39
public/app/features/alerting/unified/Silences.tsx
Normal file
39
public/app/features/alerting/unified/Silences.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { InfoBox, LoadingPlaceholder } from '@grafana/ui';
|
||||||
|
import React, { FC, useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||||
|
import { AlertManagerPicker } from './components/AlertManagerPicker';
|
||||||
|
import { useAlertManagerSourceName } from './hooks/useAlertManagerSourceName';
|
||||||
|
import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector';
|
||||||
|
import { fetchSilencesAction } from './state/actions';
|
||||||
|
import { initialAsyncRequestState } from './utils/redux';
|
||||||
|
|
||||||
|
const Silences: FC = () => {
|
||||||
|
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const silences = useUnifiedAlertingSelector((state) => state.silences);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchSilencesAction(alertManagerSourceName));
|
||||||
|
}, [alertManagerSourceName, dispatch]);
|
||||||
|
|
||||||
|
const { result, loading, error } = silences[alertManagerSourceName] || initialAsyncRequestState;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertingPageWrapper pageId="silences">
|
||||||
|
<AlertManagerPicker current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
{error && !loading && (
|
||||||
|
<InfoBox severity="error" title={<h4>Error loading silences</h4>}>
|
||||||
|
{error.message || 'Unknown error.'}
|
||||||
|
</InfoBox>
|
||||||
|
)}
|
||||||
|
{loading && <LoadingPlaceholder text="loading silences..." />}
|
||||||
|
{result && !loading && !error && <pre>{JSON.stringify(result, null, 2)}</pre>}
|
||||||
|
</AlertingPageWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Silences;
|
@ -1,5 +1,12 @@
|
|||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import {
|
||||||
|
AlertmanagerAlert,
|
||||||
|
AlertManagerCortexConfig,
|
||||||
|
AlertmanagerGroup,
|
||||||
|
Silence,
|
||||||
|
SilenceCreatePayload,
|
||||||
|
SilenceMatcher,
|
||||||
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
import { getDatasourceAPIId, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
|
|
||||||
// "grafana" for grafana-managed, otherwise a datasource name
|
// "grafana" for grafana-managed, otherwise a datasource name
|
||||||
@ -37,3 +44,75 @@ export async function updateAlertmanagerConfig(
|
|||||||
config
|
config
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchSilences(alertmanagerSourceName: string): Promise<Silence[]> {
|
||||||
|
const result = await getBackendSrv()
|
||||||
|
.fetch<Silence[]>({
|
||||||
|
url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences`,
|
||||||
|
showErrorAlert: false,
|
||||||
|
showSuccessAlert: false,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the new silence ID. Even in the case of an update, a new silence is created and the previous one expired.
|
||||||
|
export async function createOrUpdateSilence(
|
||||||
|
alertmanagerSourceName: string,
|
||||||
|
payload: SilenceCreatePayload
|
||||||
|
): Promise<string> {
|
||||||
|
const result = await getBackendSrv().post(
|
||||||
|
`/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences`,
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
return result.data.silenceID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function expireSilence(alertmanagerSourceName: string, silenceID: string): Promise<void> {
|
||||||
|
await getBackendSrv().delete(
|
||||||
|
`/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/silences/${encodeURIComponent(silenceID)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAlerts(
|
||||||
|
alertmanagerSourceName: string,
|
||||||
|
matchers?: SilenceMatcher[]
|
||||||
|
): Promise<AlertmanagerAlert[]> {
|
||||||
|
const filters =
|
||||||
|
matchers
|
||||||
|
?.map(
|
||||||
|
(matcher) =>
|
||||||
|
`filter=${encodeURIComponent(
|
||||||
|
`${escapeQuotes(matcher.name)}=${matcher.isRegex ? '~' : ''}"${escapeQuotes(matcher.value)}"`
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
.join('&') || '';
|
||||||
|
|
||||||
|
const result = await getBackendSrv()
|
||||||
|
.fetch<AlertmanagerAlert[]>({
|
||||||
|
url:
|
||||||
|
`/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/alerts` +
|
||||||
|
(filters ? '?' + filters : ''),
|
||||||
|
showErrorAlert: false,
|
||||||
|
showSuccessAlert: false,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAlertGroups(alertmanagerSourceName: string): Promise<AlertmanagerGroup[]> {
|
||||||
|
const result = await getBackendSrv()
|
||||||
|
.fetch<AlertmanagerGroup[]>({
|
||||||
|
url: `/api/alertmanager/${getDatasourceAPIId(alertmanagerSourceName)}/api/v2/alerts/groups`,
|
||||||
|
showErrorAlert: false,
|
||||||
|
showSuccessAlert: false,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeQuotes(value: string): string {
|
||||||
|
return value.replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig, Silence } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { ThunkResult } from 'app/types';
|
import { ThunkResult } from 'app/types';
|
||||||
import { RuleLocation, RuleNamespace } from 'app/types/unified-alerting';
|
import { RuleLocation, RuleNamespace } from 'app/types/unified-alerting';
|
||||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||||
import { fetchAlertManagerConfig } from '../api/alertmanager';
|
import { fetchAlertManagerConfig, fetchSilences } from '../api/alertmanager';
|
||||||
import { fetchRules } from '../api/prometheus';
|
import { fetchRules } from '../api/prometheus';
|
||||||
import { deleteRulerRulesGroup, fetchRulerRules, fetchRulerRulesNamespace, setRulerRuleGroup } from '../api/ruler';
|
import { deleteRulerRulesGroup, fetchRulerRules, fetchRulerRulesNamespace, setRulerRuleGroup } from '../api/ruler';
|
||||||
import { getAllRulesSourceNames, isCloudRulesSource } from '../utils/datasource';
|
import { getAllRulesSourceNames, isCloudRulesSource } from '../utils/datasource';
|
||||||
@ -28,6 +28,13 @@ export const fetchRulerRulesAction = createAsyncThunk(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchSilencesAction = createAsyncThunk(
|
||||||
|
'unifiedalerting/fetchSilences',
|
||||||
|
(alertManagerSourceName: string): Promise<Silence[]> => {
|
||||||
|
return withSerializedError(fetchSilences(alertManagerSourceName));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export function fetchAllPromAndRulerRules(force = false): ThunkResult<void> {
|
export function fetchAllPromAndRulerRules(force = false): ThunkResult<void> {
|
||||||
return (dispatch, getStore) => {
|
return (dispatch, getStore) => {
|
||||||
const { promRules, rulerRules } = getStore().unifiedAlerting;
|
const { promRules, rulerRules } = getStore().unifiedAlerting;
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
import { createAsyncMapSlice } from '../utils/redux';
|
import { createAsyncMapSlice } from '../utils/redux';
|
||||||
import { fetchAlertManagerConfigAction, fetchPromRulesAction, fetchRulerRulesAction } from './actions';
|
import {
|
||||||
|
fetchAlertManagerConfigAction,
|
||||||
|
fetchPromRulesAction,
|
||||||
|
fetchRulerRulesAction,
|
||||||
|
fetchSilencesAction,
|
||||||
|
} from './actions';
|
||||||
|
|
||||||
export const reducer = combineReducers({
|
export const reducer = combineReducers({
|
||||||
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, (dataSourceName) => dataSourceName).reducer,
|
promRules: createAsyncMapSlice('promRules', fetchPromRulesAction, (dataSourceName) => dataSourceName).reducer,
|
||||||
@ -10,6 +15,8 @@ export const reducer = combineReducers({
|
|||||||
fetchAlertManagerConfigAction,
|
fetchAlertManagerConfigAction,
|
||||||
(alertManagerSourceName) => alertManagerSourceName
|
(alertManagerSourceName) => alertManagerSourceName
|
||||||
).reducer,
|
).reducer,
|
||||||
|
silences: createAsyncMapSlice('silences', fetchSilencesAction, (alertManagerSourceName) => alertManagerSourceName)
|
||||||
|
.reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UnifiedAlertingState = ReturnType<typeof reducer>;
|
export type UnifiedAlertingState = ReturnType<typeof reducer>;
|
||||||
|
@ -141,3 +141,70 @@ export type AlertmanagerConfig = {
|
|||||||
inhibit_rules?: InhibitRule[];
|
inhibit_rules?: InhibitRule[];
|
||||||
receivers?: Receiver[];
|
receivers?: Receiver[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SilenceMatcher = {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
isRegex: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum SilenceState {
|
||||||
|
Active = 'active',
|
||||||
|
Expired = 'expired',
|
||||||
|
Pending = 'pending',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AlertState {
|
||||||
|
Unprocessed = 'unprocessed',
|
||||||
|
Active = 'active',
|
||||||
|
Suppressed = 'suppressed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Silence = {
|
||||||
|
id: string;
|
||||||
|
matchers?: SilenceMatcher[];
|
||||||
|
startsAt: string;
|
||||||
|
endsAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdBy: string;
|
||||||
|
comment: string;
|
||||||
|
status: {
|
||||||
|
state: SilenceState;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SilenceCreatePayload = {
|
||||||
|
id?: string;
|
||||||
|
matchers?: SilenceMatcher[];
|
||||||
|
startsAt: string;
|
||||||
|
endsAt: string;
|
||||||
|
createdBy: string;
|
||||||
|
comment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AlertmanagerAlert = {
|
||||||
|
startsAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
endsAt: string;
|
||||||
|
generatorURL?: string;
|
||||||
|
labels: { [key: string]: string };
|
||||||
|
annotations: { [key: string]: string };
|
||||||
|
receivers: [
|
||||||
|
{
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
fingerprint: string;
|
||||||
|
status: {
|
||||||
|
state: AlertState;
|
||||||
|
silencedBy: string[];
|
||||||
|
inhibitedBy: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AlertmanagerGroup = {
|
||||||
|
labels: { [key: string]: string };
|
||||||
|
receiver: { name: string };
|
||||||
|
alerts: AlertmanagerAlert[];
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
@ -363,6 +363,12 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
|
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/alerting/silences',
|
||||||
|
component: SafeDynamicImport(
|
||||||
|
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/notifications',
|
path: '/alerting/notifications',
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
|
Loading…
Reference in New Issue
Block a user