mirror of
https://github.com/prometheus/prometheus.git
synced 2025-03-05 20:59:13 -08:00
Rule panel (#6708)
* make rules content to consume groups directly Signed-off-by: blalov <boiskila@gmail.com> * create rule panel component Signed-off-by: blalov <boiskila@gmail.com> * fix react warning Signed-off-by: blalov <boiskila@gmail.com> * pr review changes Signed-off-by: blalov <boiskila@gmail.com> * lint fixes Signed-off-by: blalov <boiskila@gmail.com> * addreses PR comments Signed-off-by: Boyko Lalov <boiskila@gmail.com> * remove unnecessary double quote escaping Signed-off-by: Boyko Lalov <boiskila@gmail.com> * fix regex Signed-off-by: Boyko Lalov <boiskila@gmail.com> * apply suggested change Signed-off-by: Boyko Lalov <boiskila@gmail.com> * display duration when is > 0 Signed-off-by: Boyko Lalov <boiskila@gmail.com> * split rule type to more specific types Signed-off-by: blalov <boiskila@gmail.com> * fix typo and remove unused utils function Signed-off-by: blalov <boiskila@gmail.com> * type guard alerting rule Signed-off-by: blalov <boiskila@gmail.com> * fix ts error cause by BaseRule type Signed-off-by: blalov <boiskila@gmail.com> * handle record expression properly Signed-off-by: blalov <boiskila@gmail.com> * remove quotes escaping logic for recoreding rule Signed-off-by: blalov <boiskila@gmail.com>
This commit is contained in:
parent
eed32ef3e1
commit
d2318dc822
|
@ -3,7 +3,8 @@ import { Badge } from 'reactstrap';
|
||||||
import CollapsibleAlertPanel from './CollapsibleAlertPanel';
|
import CollapsibleAlertPanel from './CollapsibleAlertPanel';
|
||||||
import Checkbox from '../../components/Checkbox';
|
import Checkbox from '../../components/Checkbox';
|
||||||
import { isPresent } from '../../utils';
|
import { isPresent } from '../../utils';
|
||||||
import { Rule } from '../../types/types';
|
import { AlertingRule } from '../../types/types';
|
||||||
|
import { RuleGroup } from '../rules/RulesContent';
|
||||||
|
|
||||||
export type RuleState = keyof RuleStatus<any>;
|
export type RuleState = keyof RuleStatus<any>;
|
||||||
|
|
||||||
|
@ -14,7 +15,7 @@ export interface RuleStatus<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlertsProps {
|
export interface AlertsProps {
|
||||||
groups?: RuleGroup[];
|
groups?: RuleGroup<AlertingRule>[];
|
||||||
statsCount: RuleStatus<number>;
|
statsCount: RuleStatus<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,13 +27,6 @@ export interface Alert {
|
||||||
activeAt: string;
|
activeAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuleGroup {
|
|
||||||
name: string;
|
|
||||||
file: string;
|
|
||||||
rules: Rule[];
|
|
||||||
interval: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [
|
const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [
|
||||||
['inactive', 'success'],
|
['inactive', 'success'],
|
||||||
['pending', 'warning'],
|
['pending', 'warning'],
|
||||||
|
@ -101,11 +95,7 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface GroupInfoProps {
|
export const GroupInfo: FC<{ rules: AlertingRule[] }> = ({ rules, children }) => {
|
||||||
rules: Rule[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GroupInfo: FC<GroupInfoProps> = ({ rules, children }) => {
|
|
||||||
const statesCounter = rules.reduce<any>(
|
const statesCounter = rules.reduce<any>(
|
||||||
(acc, r) => {
|
(acc, r) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import React, { FC, useState, Fragment } from 'react';
|
import React, { FC, useState, Fragment } from 'react';
|
||||||
import { Link } from '@reach/router';
|
|
||||||
import { Alert, Collapse, Table, Badge } from 'reactstrap';
|
import { Alert, Collapse, Table, Badge } from 'reactstrap';
|
||||||
import { RuleStatus } from './AlertContents';
|
import { RuleStatus } from './AlertContents';
|
||||||
import { Rule } from '../../types/types';
|
import { AlertingRule } from '../../types/types';
|
||||||
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { createExpressionLink } from '../../utils/index';
|
import { RulePanel } from '../rules/RulePanel';
|
||||||
|
|
||||||
interface CollapsibleAlertPanelProps {
|
interface CollapsibleAlertPanelProps {
|
||||||
rule: Rule;
|
rule: AlertingRule;
|
||||||
showAnnotations: boolean;
|
showAnnotations: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,27 +24,10 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
|
||||||
<>
|
<>
|
||||||
<Alert fade={false} onClick={() => toggle(!open)} color={alertColors[rule.state]} style={{ cursor: 'pointer' }}>
|
<Alert fade={false} onClick={() => toggle(!open)} color={alertColors[rule.state]} style={{ cursor: 'pointer' }}>
|
||||||
<FontAwesomeIcon icon={open ? faChevronDown : faChevronRight} fixedWidth />
|
<FontAwesomeIcon icon={open ? faChevronDown : faChevronRight} fixedWidth />
|
||||||
<strong>{rule.name}</strong> ({`${rule.alerts.length} active`})
|
<strong>{rule.name}</strong> ({rule.alerts.length} active)
|
||||||
</Alert>
|
</Alert>
|
||||||
<Collapse isOpen={open} className="mb-2">
|
<Collapse isOpen={open} className="mb-2">
|
||||||
<pre style={{ background: '#f5f5f5', padding: 15 }}>
|
<RulePanel rule={rule} styles={{ padding: 15, border: '1px solid #ccc', borderRadius: 4 }} />
|
||||||
<code>
|
|
||||||
<div>
|
|
||||||
name: <Link to={createExpressionLink(`ALERTS{alertname="${rule.name}"}`)}>{rule.name}</Link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
expr: <Link to={createExpressionLink(rule.query)}>{rule.query}</Link>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>labels:</div>
|
|
||||||
<div className="ml-5">severity: {rule.labels.severity}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div>annotations:</div>
|
|
||||||
<div className="ml-5">summary: {rule.annotations.summary}</div>
|
|
||||||
</div>
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
{rule.alerts.length > 0 && (
|
{rule.alerts.length > 0 && (
|
||||||
<Table bordered size="sm">
|
<Table bordered size="sm">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -71,11 +53,9 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<h5 className="m-0">
|
<Badge color={alertColors[alert.state] + ' text-uppercase'} className="px-3 py-2">
|
||||||
<Badge color={alertColors[alert.state] + ' text-uppercase'} className="px-3">
|
{alert.state}
|
||||||
{alert.state}
|
</Badge>
|
||||||
</Badge>
|
|
||||||
</h5>
|
|
||||||
</td>
|
</td>
|
||||||
<td>{alert.activeAt}</td>
|
<td>{alert.activeAt}</td>
|
||||||
<td>{alert.value}</td>
|
<td>{alert.value}</td>
|
||||||
|
|
54
web/ui/react-app/src/pages/rules/RulePanel.tsx
Normal file
54
web/ui/react-app/src/pages/rules/RulePanel.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import React, { FC, CSSProperties, Fragment } from 'react';
|
||||||
|
import { Rule } from '../../types/types';
|
||||||
|
import { Link } from '@reach/router';
|
||||||
|
import { createExpressionLink, mapObjEntries, formatRange } from '../../utils';
|
||||||
|
|
||||||
|
interface RulePanelProps {
|
||||||
|
rule: Rule;
|
||||||
|
styles?: CSSProperties;
|
||||||
|
tag?: keyof JSX.IntrinsicElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RulePanel: FC<RulePanelProps> = ({ rule, styles = {}, tag }) => {
|
||||||
|
const Tag = tag || Fragment;
|
||||||
|
const { name, query, labels } = rule;
|
||||||
|
const style = { background: '#f5f5f5', ...styles };
|
||||||
|
return (
|
||||||
|
<Tag {...(tag ? { style } : {})}>
|
||||||
|
<pre className="m-0" style={tag ? undefined : style}>
|
||||||
|
<code>
|
||||||
|
{rule.type === 'alerting' ? 'alert: ' : 'record: '}
|
||||||
|
<Link
|
||||||
|
to={createExpressionLink(rule.type === 'alerting' ? `ALERTS{alertname="${name.replace(/"/g, '\\"')}"}` : name)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
expr: <Link to={createExpressionLink(query)}>{query}</Link>
|
||||||
|
</div>
|
||||||
|
{rule.type === 'alerting' && rule.duration > 0 && <div>for: {formatRange(rule.duration)}</div>}
|
||||||
|
{labels && (
|
||||||
|
<>
|
||||||
|
labels:
|
||||||
|
{mapObjEntries(labels, ([key, value]) => (
|
||||||
|
<div className="ml-4" key={key}>
|
||||||
|
{key}: {value}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{rule.type === 'alerting' && rule.annotations && (
|
||||||
|
<>
|
||||||
|
annotations:
|
||||||
|
{mapObjEntries(rule.annotations, ([key, value]) => (
|
||||||
|
<div className="ml-4" key={key}>
|
||||||
|
{key}: {value}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,14 +3,14 @@ import { RouteComponentProps } from '@reach/router';
|
||||||
import PathPrefixProps from '../../types/PathPrefixProps';
|
import PathPrefixProps from '../../types/PathPrefixProps';
|
||||||
import { useFetch } from '../../hooks/useFetch';
|
import { useFetch } from '../../hooks/useFetch';
|
||||||
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||||
import { RulesMap, RulesContent } from './RulesContent';
|
import { RulesGroups, RulesContent } from './RulesContent';
|
||||||
|
|
||||||
const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
|
const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
|
||||||
|
|
||||||
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
||||||
const { response, error, isLoading } = useFetch<RulesMap>(`${pathPrefix}/api/v1/rules`);
|
const { response, error, isLoading } = useFetch<RulesGroups>(`${pathPrefix}/api/v1/rules`);
|
||||||
|
|
||||||
return <RulesWithStatusIndicator response={response} error={error} isLoading={isLoading} />;
|
return <RulesWithStatusIndicator {...response.data} error={error} isLoading={isLoading} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Rules;
|
export default Rules;
|
||||||
|
|
|
@ -1,130 +1,82 @@
|
||||||
import React, { FC } from 'react';
|
import React, { FC, Fragment } from 'react';
|
||||||
import { RouteComponentProps } from '@reach/router';
|
import { RouteComponentProps } from '@reach/router';
|
||||||
import { APIResponse } from '../../hooks/useFetch';
|
|
||||||
import { Alert, Table, Badge } from 'reactstrap';
|
import { Alert, Table, Badge } from 'reactstrap';
|
||||||
import { Link } from '@reach/router';
|
import { formatRelative, humanizeDuration, isPresent } from '../../utils';
|
||||||
import { formatRelative, createExpressionLink, humanizeDuration } from '../../utils';
|
|
||||||
import { Rule } from '../../types/types';
|
|
||||||
import { now } from 'moment';
|
import { now } from 'moment';
|
||||||
|
import { RulePanel } from './RulePanel';
|
||||||
|
import { Rule } from '../../types/types';
|
||||||
|
|
||||||
interface RulesContentProps {
|
export interface RuleGroup<T> {
|
||||||
response: APIResponse<RulesMap>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RuleGroup {
|
|
||||||
name: string;
|
name: string;
|
||||||
file: string;
|
file: string;
|
||||||
rules: Rule[];
|
rules: T[];
|
||||||
|
interval: number;
|
||||||
evaluationTime: string;
|
evaluationTime: string;
|
||||||
lastEvaluation: string;
|
lastEvaluation: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RulesMap {
|
export interface RulesGroups {
|
||||||
groups: RuleGroup[];
|
groups: RuleGroup<Rule>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const GraphExpressionLink: FC<{ expr: string; title: string }> = props => {
|
export const badgeColorMap: Record<'ok' | 'err' | 'unknown', 'success' | 'danger' | 'warning'> = {
|
||||||
|
ok: 'success',
|
||||||
|
err: 'danger',
|
||||||
|
unknown: 'warning',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RulesContent: FC<RouteComponentProps & RulesGroups> = ({ groups }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<strong>{props.title}:</strong> <Link to={createExpressionLink(props.expr)}>{props.expr}</Link>
|
<h2>Rules</h2>
|
||||||
<br />
|
<Table bordered>
|
||||||
|
{groups.map(group => {
|
||||||
|
const { name: groupName, lastEvaluation, evaluationTime } = group;
|
||||||
|
return (
|
||||||
|
<Fragment key={groupName}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={3}>
|
||||||
|
<a href={`#${groupName}`}>
|
||||||
|
<h2 id={groupName}>{groupName}</h2>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<h2>{formatRelative(lastEvaluation, now())} ago</h2>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<h2>{humanizeDuration(parseFloat(evaluationTime) * 1000)}</h2>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr className="font-weight-bold">
|
||||||
|
<td>Rule</td>
|
||||||
|
<td>State</td>
|
||||||
|
<td>Error</td>
|
||||||
|
<td>Last Evaluation</td>
|
||||||
|
<td>Evaluation Time</td>
|
||||||
|
</tr>
|
||||||
|
{group.rules.map((rule, i) => {
|
||||||
|
return (
|
||||||
|
<tr key={i}>
|
||||||
|
<RulePanel tag="td" rule={rule} />
|
||||||
|
<td style={{ textAlign: 'center', verticalAlign: 'middle' }}>
|
||||||
|
<Badge className="p-2 px-4 text-uppercase" color={badgeColorMap[rule.health]}>
|
||||||
|
{rule.health}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td>{isPresent(rule.lastError) && <Alert color="danger">{rule.lastError}</Alert>}</td>
|
||||||
|
<td>{formatRelative(rule.lastEvaluation, now())} ago</td>
|
||||||
|
<td>{humanizeDuration(parseFloat(rule.evaluationTime) * 1000)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Table>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RulesContent: FC<RouteComponentProps & RulesContentProps> = ({ response }) => {
|
|
||||||
const getBadgeColor = (state: string) => {
|
|
||||||
switch (state) {
|
|
||||||
case 'ok':
|
|
||||||
return 'success';
|
|
||||||
|
|
||||||
case 'err':
|
|
||||||
return 'danger';
|
|
||||||
|
|
||||||
case 'unknown':
|
|
||||||
return 'warning';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (response.data) {
|
|
||||||
const groups: RuleGroup[] = response.data.groups;
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2>Rules</h2>
|
|
||||||
<Table bordered>
|
|
||||||
{groups.map((g, i) => {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={i}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td colSpan={3}>
|
|
||||||
<a href={'#' + g.name}>
|
|
||||||
<h2 id={g.name}>{g.name}</h2>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<h2>{formatRelative(g.lastEvaluation, now())} ago</h2>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<h2>{humanizeDuration(parseFloat(g.evaluationTime) * 1000)}</h2>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr className="font-weight-bold">
|
|
||||||
<td>Rule</td>
|
|
||||||
<td>State</td>
|
|
||||||
<td>Error</td>
|
|
||||||
<td>Last Evaluation</td>
|
|
||||||
<td>Evaluation Time</td>
|
|
||||||
</tr>
|
|
||||||
{g.rules.map((r, i) => {
|
|
||||||
return (
|
|
||||||
<tr key={i}>
|
|
||||||
{r.alerts ? (
|
|
||||||
<td className="rule-cell">
|
|
||||||
<GraphExpressionLink title="alert" expr={r.name} />
|
|
||||||
<GraphExpressionLink title="expr" expr={r.query} />
|
|
||||||
<div>
|
|
||||||
<strong>labels:</strong>
|
|
||||||
{Object.entries(r.labels).map(([key, value]) => (
|
|
||||||
<div className="ml-4" key={key}>
|
|
||||||
{key}: {value}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>annotations:</strong>
|
|
||||||
{Object.entries(r.annotations).map(([key, value]) => (
|
|
||||||
<div className="ml-4" key={key}>
|
|
||||||
{key}: {value}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
) : (
|
|
||||||
<td className="rule-cell">
|
|
||||||
<GraphExpressionLink title="record" expr={r.name} />
|
|
||||||
<GraphExpressionLink title="expr" expr={r.query} />
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td>
|
|
||||||
<Badge color={getBadgeColor(r.health)}>{r.health.toUpperCase()}</Badge>
|
|
||||||
</td>
|
|
||||||
<td>{r.lastError ? <Alert color="danger">{r.lastError}</Alert> : null}</td>
|
|
||||||
<td>{formatRelative(r.lastEvaluation, now())} ago</td>
|
|
||||||
<td>{humanizeDuration(parseFloat(r.evaluationTime) * 1000)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
|
@ -10,17 +10,26 @@ export interface QueryParams {
|
||||||
resolution: number;
|
resolution: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Rule {
|
export interface BaseRule {
|
||||||
alerts: Alert[];
|
|
||||||
annotations: Record<string, string>;
|
|
||||||
duration: number;
|
|
||||||
evaluationTime: string;
|
|
||||||
health: string;
|
|
||||||
labels: Record<string, string>;
|
|
||||||
lastError?: string;
|
|
||||||
lastEvaluation: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
query: string;
|
query: string;
|
||||||
state: RuleState;
|
labels: Record<string, string>;
|
||||||
type: string;
|
health: 'ok' | 'err' | 'unknown';
|
||||||
|
lastError?: string;
|
||||||
|
evaluationTime: string;
|
||||||
|
lastEvaluation: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AlertingRule extends BaseRule {
|
||||||
|
alerts: Alert[];
|
||||||
|
duration: number;
|
||||||
|
state: RuleState;
|
||||||
|
annotations: Record<string, string>;
|
||||||
|
type: 'alerting';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecordingRule extends BaseRule {
|
||||||
|
type: 'recording';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Rule = RecordingRule | AlertingRule;
|
||||||
|
|
|
@ -207,6 +207,5 @@ export const mapObjEntries = <T, key extends keyof T, Z>(
|
||||||
) => Object.entries(o).map(cb);
|
) => Object.entries(o).map(cb);
|
||||||
|
|
||||||
export const callAll = (...fns: Array<(...args: any) => void>) => (...args: any) => {
|
export const callAll = (...fns: Array<(...args: any) => void>) => (...args: any) => {
|
||||||
// eslint-disable-next-line prefer-spread
|
fns.filter(Boolean).forEach(fn => fn(...args));
|
||||||
fns.filter(Boolean).forEach(fn => fn.apply(null, args));
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue