mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
WIP
This commit is contained in:
parent
c16244234e
commit
80b8879d4e
@ -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",
|
||||||
|
@ -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',
|
||||||
|
151
public/app/features/alerting/unified/CentralAlertHistory.tsx
Normal file
151
public/app/features/alerting/unified/CentralAlertHistory.tsx
Normal 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' });
|
@ -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 },
|
||||||
|
@ -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) => {
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user