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 Checkbox from '../../components/Checkbox';
|
||||
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>;
|
||||
|
||||
|
@ -14,7 +15,7 @@ export interface RuleStatus<T> {
|
|||
}
|
||||
|
||||
export interface AlertsProps {
|
||||
groups?: RuleGroup[];
|
||||
groups?: RuleGroup<AlertingRule>[];
|
||||
statsCount: RuleStatus<number>;
|
||||
}
|
||||
|
||||
|
@ -26,13 +27,6 @@ export interface Alert {
|
|||
activeAt: string;
|
||||
}
|
||||
|
||||
interface RuleGroup {
|
||||
name: string;
|
||||
file: string;
|
||||
rules: Rule[];
|
||||
interval: number;
|
||||
}
|
||||
|
||||
const stateColorTuples: Array<[RuleState, 'success' | 'warning' | 'danger']> = [
|
||||
['inactive', 'success'],
|
||||
['pending', 'warning'],
|
||||
|
@ -101,11 +95,7 @@ const AlertsContent: FC<AlertsProps> = ({ groups = [], statsCount }) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface GroupInfoProps {
|
||||
rules: Rule[];
|
||||
}
|
||||
|
||||
export const GroupInfo: FC<GroupInfoProps> = ({ rules, children }) => {
|
||||
export const GroupInfo: FC<{ rules: AlertingRule[] }> = ({ rules, children }) => {
|
||||
const statesCounter = rules.reduce<any>(
|
||||
(acc, r) => {
|
||||
return {
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import React, { FC, useState, Fragment } from 'react';
|
||||
import { Link } from '@reach/router';
|
||||
import { Alert, Collapse, Table, Badge } from 'reactstrap';
|
||||
import { RuleStatus } from './AlertContents';
|
||||
import { Rule } from '../../types/types';
|
||||
import { AlertingRule } from '../../types/types';
|
||||
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { createExpressionLink } from '../../utils/index';
|
||||
import { RulePanel } from '../rules/RulePanel';
|
||||
|
||||
interface CollapsibleAlertPanelProps {
|
||||
rule: Rule;
|
||||
rule: AlertingRule;
|
||||
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' }}>
|
||||
<FontAwesomeIcon icon={open ? faChevronDown : faChevronRight} fixedWidth />
|
||||
<strong>{rule.name}</strong> ({`${rule.alerts.length} active`})
|
||||
<strong>{rule.name}</strong> ({rule.alerts.length} active)
|
||||
</Alert>
|
||||
<Collapse isOpen={open} className="mb-2">
|
||||
<pre style={{ background: '#f5f5f5', padding: 15 }}>
|
||||
<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>
|
||||
<RulePanel rule={rule} styles={{ padding: 15, border: '1px solid #ccc', borderRadius: 4 }} />
|
||||
{rule.alerts.length > 0 && (
|
||||
<Table bordered size="sm">
|
||||
<thead>
|
||||
|
@ -71,11 +53,9 @@ const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnot
|
|||
})}
|
||||
</td>
|
||||
<td>
|
||||
<h5 className="m-0">
|
||||
<Badge color={alertColors[alert.state] + ' text-uppercase'} className="px-3">
|
||||
<Badge color={alertColors[alert.state] + ' text-uppercase'} className="px-3 py-2">
|
||||
{alert.state}
|
||||
</Badge>
|
||||
</h5>
|
||||
</td>
|
||||
<td>{alert.activeAt}</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 { useFetch } from '../../hooks/useFetch';
|
||||
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||
import { RulesMap, RulesContent } from './RulesContent';
|
||||
import { RulesGroups, RulesContent } from './RulesContent';
|
||||
|
||||
const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
|
||||
|
||||
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;
|
||||
|
|
|
@ -1,72 +1,51 @@
|
|||
import React, { FC } from 'react';
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import { APIResponse } from '../../hooks/useFetch';
|
||||
import { Alert, Table, Badge } from 'reactstrap';
|
||||
import { Link } from '@reach/router';
|
||||
import { formatRelative, createExpressionLink, humanizeDuration } from '../../utils';
|
||||
import { Rule } from '../../types/types';
|
||||
import { formatRelative, humanizeDuration, isPresent } from '../../utils';
|
||||
import { now } from 'moment';
|
||||
import { RulePanel } from './RulePanel';
|
||||
import { Rule } from '../../types/types';
|
||||
|
||||
interface RulesContentProps {
|
||||
response: APIResponse<RulesMap>;
|
||||
}
|
||||
|
||||
interface RuleGroup {
|
||||
export interface RuleGroup<T> {
|
||||
name: string;
|
||||
file: string;
|
||||
rules: Rule[];
|
||||
rules: T[];
|
||||
interval: number;
|
||||
evaluationTime: string;
|
||||
lastEvaluation: string;
|
||||
}
|
||||
|
||||
export interface RulesMap {
|
||||
groups: RuleGroup[];
|
||||
export interface RulesGroups {
|
||||
groups: RuleGroup<Rule>[];
|
||||
}
|
||||
|
||||
const GraphExpressionLink: FC<{ expr: string; title: string }> = props => {
|
||||
return (
|
||||
<>
|
||||
<strong>{props.title}:</strong> <Link to={createExpressionLink(props.expr)}>{props.expr}</Link>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
export const badgeColorMap: Record<'ok' | 'err' | 'unknown', 'success' | 'danger' | 'warning'> = {
|
||||
ok: 'success',
|
||||
err: 'danger',
|
||||
unknown: 'warning',
|
||||
};
|
||||
|
||||
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;
|
||||
export const RulesContent: FC<RouteComponentProps & RulesGroups> = ({ groups }) => {
|
||||
return (
|
||||
<>
|
||||
<h2>Rules</h2>
|
||||
<Table bordered>
|
||||
{groups.map((g, i) => {
|
||||
{groups.map(group => {
|
||||
const { name: groupName, lastEvaluation, evaluationTime } = group;
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
<Fragment key={groupName}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<a href={'#' + g.name}>
|
||||
<h2 id={g.name}>{g.name}</h2>
|
||||
<a href={`#${groupName}`}>
|
||||
<h2 id={groupName}>{groupName}</h2>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<h2>{formatRelative(g.lastEvaluation, now())} ago</h2>
|
||||
<h2>{formatRelative(lastEvaluation, now())} ago</h2>
|
||||
</td>
|
||||
<td>
|
||||
<h2>{humanizeDuration(parseFloat(g.evaluationTime) * 1000)}</h2>
|
||||
<h2>{humanizeDuration(parseFloat(evaluationTime) * 1000)}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -78,53 +57,26 @@ export const RulesContent: FC<RouteComponentProps & RulesContentProps> = ({ resp
|
|||
<td>Last Evaluation</td>
|
||||
<td>Evaluation Time</td>
|
||||
</tr>
|
||||
{g.rules.map((r, i) => {
|
||||
{group.rules.map((rule, 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>
|
||||
<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 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>
|
||||
<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>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -10,17 +10,26 @@ export interface QueryParams {
|
|||
resolution: number;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
alerts: Alert[];
|
||||
annotations: Record<string, string>;
|
||||
duration: number;
|
||||
evaluationTime: string;
|
||||
health: string;
|
||||
labels: Record<string, string>;
|
||||
lastError?: string;
|
||||
lastEvaluation: string;
|
||||
export interface BaseRule {
|
||||
name: string;
|
||||
query: string;
|
||||
state: RuleState;
|
||||
type: string;
|
||||
labels: Record<string, 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);
|
||||
|
||||
export const callAll = (...fns: Array<(...args: any) => void>) => (...args: any) => {
|
||||
// eslint-disable-next-line prefer-spread
|
||||
fns.filter(Boolean).forEach(fn => fn.apply(null, args));
|
||||
fns.filter(Boolean).forEach(fn => fn(...args));
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue