This commit is contained in:
Sonia Aguilar 2024-05-29 15:29:49 +02:00
parent c16244234e
commit 80b8879d4e
6 changed files with 181 additions and 3 deletions

View File

@ -401,6 +401,16 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts from an Alertmanager instance", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"}) alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts from an Alertmanager instance", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"})
} }
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingCentralAlertHistory) {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Alert history",
SubTitle: "History of events that were generated by by your Grafana-managed alert rules. Silences and Mute timmings are ignored.",
Id: "alerts-history",
Url: s.cfg.AppSubURL + "/alerting/history",
Icon: "history",
})
}
if c.SignedInUser.GetOrgRole() == org.RoleAdmin { if c.SignedInUser.GetOrgRole() == org.RoleAdmin {
alertChildNavs = append(alertChildNavs, &navtree.NavLink{ alertChildNavs = append(alertChildNavs, &navtree.NavLink{
Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin", Text: "Settings", Id: "alerting-admin", Url: s.cfg.AppSubURL + "/alerting/admin",

View File

@ -169,6 +169,16 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups') () => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
), ),
}, },
{
path: '/alerting/history/',
roles: evaluateAccess([
AccessControlAction.AlertingInstanceRead,
AccessControlAction.AlertingInstancesExternalRead,
]),
component: importAlertingComponent(
() => import(/* webpackChunkName: "CentralAlertHistory" */ 'app/features/alerting/unified/CentralAlertHistory')
),
},
{ {
path: '/alerting/new/:type?', path: '/alerting/new/:type?',
pageClass: 'page-alerting', pageClass: 'page-alerting',

View File

@ -0,0 +1,151 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { NavModelItem, getDefaultTimeRange } from '@grafana/data';
import { isFetchError } from '@grafana/runtime';
import { Alert, Button, Field, Icon, Input, Label, Stack, withErrorBoundary } from '@grafana/ui';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
import { stateHistoryApi } from './api/stateHistoryApi';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { STATE_HISTORY_POLLING_INTERVAL } from './components/rules/state-history/LokiStateHistory';
import { LogRecord } from './components/rules/state-history/common';
import { useRuleHistoryRecords } from './components/rules/state-history/useRuleHistoryRecords';
import { stringifyErrorLike } from './utils/misc';
const CentralAlertHistory = (): JSX.Element => {
const { useGetRuleHistoryQuery } = stateHistoryApi;
// Filter state
const [eventsFilter, setEventsFilter] = useState('');
const { getValues, setValue, register, handleSubmit } = useForm({ defaultValues: { query: '' } }); // form for search field
const onFilterCleared = useCallback(() => {
setEventsFilter('');
setValue('query', '');
}, [setEventsFilter, setValue]);
// We prefer log count-based limit rather than time-based, but the API doesn't support it yet
const queryTimeRange = useMemo(() => getDefaultTimeRange(), []);
const {
currentData: stateHistory,
isLoading,
isError,
error,
} = useGetRuleHistoryQuery(
{
from: queryTimeRange.from.unix(),
to: queryTimeRange.to.unix(),
limit: 250,
},
{
refetchOnFocus: true,
refetchOnReconnect: true,
pollingInterval: STATE_HISTORY_POLLING_INTERVAL,
}
);
const { historyRecords, findCommonLabels } = useRuleHistoryRecords(
stateHistory
// instancesFilter
);
const defaultPageNav: NavModelItem = {
id: 'alerts-history',
text: '',
};
if (isError) {
return (
<AlertingPageWrapper pageNav={defaultPageNav} navId="alerts-history">
<HistoryErrorMessage error={error} />
</AlertingPageWrapper>
);
}
if (isLoading) {
return (
<AlertingPageWrapper pageNav={defaultPageNav} navId="alerts-history" isLoading={true}>
<></>
</AlertingPageWrapper>
);
}
return (
<AlertingPageWrapper navId="alerts-history" isLoading={false}>
<Stack direction="column" gap={1}>
<form onSubmit={handleSubmit((data) => setEventsFilter(data.query))}>
<SearchFieldInput
{...register('query')}
showClearFilterSuffix={!!eventsFilter}
onClearFilterClick={onFilterCleared}
/>
<input type="submit" hidden />
</form>
<HistoryLogEntry logRecords={historyRecords} />
</Stack>
</AlertingPageWrapper>
);
};
interface HistoryLogEntryProps {
logRecords: LogRecord[];
}
function HistoryLogEntry({ logRecords }: HistoryLogEntryProps) {
// display log records
return (
<ul>
{logRecords.map((record, index) => {
return (
<li key={index}>
{record.timestamp} - {JSON.stringify(record.line.previous)} {`->`} {JSON.stringify(record.line.current)}-{' '}
{JSON.stringify(record.line.labels)}
</li>
);
})}
</ul>
);
}
interface HistoryErrorMessageProps {
error: unknown;
}
function HistoryErrorMessage({ error }: HistoryErrorMessageProps) {
if (isFetchError(error) && error.status === 404) {
return <EntityNotFound entity="History" />;
}
return <Alert title={'Something went wrong loading the alert state history'}>{stringifyErrorLike(error)}</Alert>;
}
interface SearchFieldInputProps {
showClearFilterSuffix: boolean;
onClearFilterClick: () => void;
}
const SearchFieldInput = ({ showClearFilterSuffix, onClearFilterClick }: SearchFieldInputProps) => {
return (
<Field
label={
<Label htmlFor="instancesSearchInput">
<Stack gap={0.5}>
<span>Filter instances</span>
</Stack>
</Label>
}
>
<Input
id="eventsSearchInput"
prefix={<Icon name="search" />}
suffix={
showClearFilterSuffix && (
<Button fill="text" icon="times" size="sm" onClick={onClearFilterClick}>
Clear
</Button>
)
}
placeholder="Filter events"
/>
</Field>
);
};
export default withErrorBoundary(CentralAlertHistory, { style: 'page' });

View File

@ -4,7 +4,7 @@ import { alertingApi } from './alertingApi';
export const stateHistoryApi = alertingApi.injectEndpoints({ export const stateHistoryApi = alertingApi.injectEndpoints({
endpoints: (build) => ({ endpoints: (build) => ({
getRuleHistory: build.query<DataFrameJSON, { ruleUid: string; from?: number; to?: number; limit?: number }>({ getRuleHistory: build.query<DataFrameJSON, { ruleUid?: string; from?: number; to?: number; limit?: number }>({
query: ({ ruleUid, from, to, limit = 100 }) => ({ query: ({ ruleUid, from, to, limit = 100 }) => ({
url: '/api/v1/rules/history', url: '/api/v1/rules/history',
params: { ruleUID: ruleUid, from, to, limit }, params: { ruleUID: ruleUid, from, to, limit },

View File

@ -4,7 +4,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data'; import { DataFrame, dateTime, GrafanaTheme2, TimeRange } from '@grafana/data';
import { Alert, Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui'; import { Alert, Button, Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { stateHistoryApi } from '../../../api/stateHistoryApi'; import { stateHistoryApi } from '../../../api/stateHistoryApi';
import { combineMatcherStrings } from '../../../utils/alertmanager'; import { combineMatcherStrings } from '../../../utils/alertmanager';
@ -19,7 +19,7 @@ interface Props {
ruleUID: string; ruleUID: string;
} }
const STATE_HISTORY_POLLING_INTERVAL = 10 * 1000; // 10 seconds export const STATE_HISTORY_POLLING_INTERVAL = 10 * 1000; // 10 seconds
const MAX_TIMELINE_SERIES = 12; const MAX_TIMELINE_SERIES = 12;
const LokiStateHistory = ({ ruleUID }: Props) => { const LokiStateHistory = ({ ruleUID }: Props) => {

View File

@ -149,6 +149,13 @@ export const navIndex: NavIndex = {
icon: 'layer-group', icon: 'layer-group',
url: '/alerting/groups', url: '/alerting/groups',
}, },
{
id: 'history',
text: 'History',
subTitle: 'Alert state history',
icon: 'history',
url: '/alerting/history',
},
{ {
id: 'alerting-admin', id: 'alerting-admin',
text: 'Settings', text: 'Settings',