mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-24 21:24:05 -08:00
Adds support for rules screen in react-ui (#6503)
* base Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * base of rules page Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * initial version Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * removed unused function Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * version 1 Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions. Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * new fetching pattern Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com> * implemented suggestions Signed-off-by: Harkishen Singh <harkishensingh@hotmail.com>
This commit is contained in:
parent
d996ba20ec
commit
c1e49d50c5
|
@ -897,33 +897,39 @@ type RuleGroup struct {
|
|||
// In order to preserve rule ordering, while exposing type (alerting or recording)
|
||||
// specific properties, both alerting and recording rules are exposed in the
|
||||
// same array.
|
||||
Rules []rule `json:"rules"`
|
||||
Interval float64 `json:"interval"`
|
||||
Rules []rule `json:"rules"`
|
||||
Interval float64 `json:"interval"`
|
||||
EvaluationTime float64 `json:"evaluationTime"`
|
||||
LastEvaluation time.Time `json:"lastEvaluation"`
|
||||
}
|
||||
|
||||
type rule interface{}
|
||||
|
||||
type alertingRule struct {
|
||||
// State can be "pending", "firing", "inactive".
|
||||
State string `json:"state"`
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
Duration float64 `json:"duration"`
|
||||
Labels labels.Labels `json:"labels"`
|
||||
Annotations labels.Labels `json:"annotations"`
|
||||
Alerts []*Alert `json:"alerts"`
|
||||
Health rules.RuleHealth `json:"health"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
State string `json:"state"`
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
Duration float64 `json:"duration"`
|
||||
Labels labels.Labels `json:"labels"`
|
||||
Annotations labels.Labels `json:"annotations"`
|
||||
Alerts []*Alert `json:"alerts"`
|
||||
Health rules.RuleHealth `json:"health"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
EvaluationTime float64 `json:"evaluationTime"`
|
||||
LastEvaluation time.Time `json:"lastEvaluation"`
|
||||
// Type of an alertingRule is always "alerting".
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type recordingRule struct {
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
Labels labels.Labels `json:"labels,omitempty"`
|
||||
Health rules.RuleHealth `json:"health"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
Labels labels.Labels `json:"labels,omitempty"`
|
||||
Health rules.RuleHealth `json:"health"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
EvaluationTime float64 `json:"evaluationTime"`
|
||||
LastEvaluation time.Time `json:"lastEvaluation"`
|
||||
// Type of a recordingRule is always "recording".
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
@ -943,10 +949,12 @@ func (api *API) rules(r *http.Request) apiFuncResult {
|
|||
|
||||
for i, grp := range ruleGroups {
|
||||
apiRuleGroup := &RuleGroup{
|
||||
Name: grp.Name(),
|
||||
File: grp.File(),
|
||||
Interval: grp.Interval().Seconds(),
|
||||
Rules: []rule{},
|
||||
Name: grp.Name(),
|
||||
File: grp.File(),
|
||||
Interval: grp.Interval().Seconds(),
|
||||
Rules: []rule{},
|
||||
EvaluationTime: grp.GetEvaluationDuration().Seconds(),
|
||||
LastEvaluation: grp.GetEvaluationTimestamp(),
|
||||
}
|
||||
for _, r := range grp.Rules() {
|
||||
var enrichedRule rule
|
||||
|
@ -961,28 +969,32 @@ func (api *API) rules(r *http.Request) apiFuncResult {
|
|||
break
|
||||
}
|
||||
enrichedRule = alertingRule{
|
||||
State: rule.State().String(),
|
||||
Name: rule.Name(),
|
||||
Query: rule.Query().String(),
|
||||
Duration: rule.Duration().Seconds(),
|
||||
Labels: rule.Labels(),
|
||||
Annotations: rule.Annotations(),
|
||||
Alerts: rulesAlertsToAPIAlerts(rule.ActiveAlerts()),
|
||||
Health: rule.Health(),
|
||||
LastError: lastError,
|
||||
Type: "alerting",
|
||||
State: rule.State().String(),
|
||||
Name: rule.Name(),
|
||||
Query: rule.Query().String(),
|
||||
Duration: rule.Duration().Seconds(),
|
||||
Labels: rule.Labels(),
|
||||
Annotations: rule.Annotations(),
|
||||
Alerts: rulesAlertsToAPIAlerts(rule.ActiveAlerts()),
|
||||
Health: rule.Health(),
|
||||
LastError: lastError,
|
||||
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
|
||||
LastEvaluation: rule.GetEvaluationTimestamp(),
|
||||
Type: "alerting",
|
||||
}
|
||||
case *rules.RecordingRule:
|
||||
if !returnRecording {
|
||||
break
|
||||
}
|
||||
enrichedRule = recordingRule{
|
||||
Name: rule.Name(),
|
||||
Query: rule.Query().String(),
|
||||
Labels: rule.Labels(),
|
||||
Health: rule.Health(),
|
||||
LastError: lastError,
|
||||
Type: "recording",
|
||||
Name: rule.Name(),
|
||||
Query: rule.Query().String(),
|
||||
Labels: rule.Labels(),
|
||||
Health: rule.Health(),
|
||||
LastError: lastError,
|
||||
EvaluationTime: rule.GetEvaluationDuration().Seconds(),
|
||||
LastEvaluation: rule.GetEvaluationTimestamp(),
|
||||
Type: "recording",
|
||||
}
|
||||
default:
|
||||
err := errors.Errorf("failed to assert type of rule '%v'", rule.Name())
|
||||
|
|
|
@ -237,3 +237,14 @@ button.execute-btn {
|
|||
margin-right: 5px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.rules-head {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rule_cell {
|
||||
white-space: pre-wrap;
|
||||
background-color: #F5F5F5;
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
|
@ -3,21 +3,10 @@ import { Badge } from 'reactstrap';
|
|||
import CollapsibleAlertPanel from './CollapsibleAlertPanel';
|
||||
import Checkbox from '../../components/Checkbox';
|
||||
import { isPresent } from '../../utils';
|
||||
import { Rule } from '../../types/types';
|
||||
|
||||
export type RuleState = keyof RuleStatus<any>;
|
||||
|
||||
export interface Rule {
|
||||
alerts: Alert[];
|
||||
annotations: Record<string, string>;
|
||||
duration: number;
|
||||
health: string;
|
||||
labels: Record<string, string>;
|
||||
name: string;
|
||||
query: string;
|
||||
state: RuleState;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface RuleStatus<T> {
|
||||
firing: T;
|
||||
pending: T;
|
||||
|
@ -29,7 +18,7 @@ export interface AlertsProps {
|
|||
statsCount: RuleStatus<number>;
|
||||
}
|
||||
|
||||
interface Alert {
|
||||
export interface Alert {
|
||||
labels: Record<string, string>;
|
||||
state: RuleState;
|
||||
value: string;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import React, { FC, useState, Fragment } from 'react';
|
||||
import { Link } from '@reach/router';
|
||||
import { Alert, Collapse, Table, Badge } from 'reactstrap';
|
||||
import { Rule, RuleStatus } from './AlertContents';
|
||||
import { RuleStatus } from './AlertContents';
|
||||
import { Rule } from '../../types/types';
|
||||
import { faChevronDown, faChevronRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { createExpressionLink } from '../../utils/index';
|
||||
|
||||
interface CollapsibleAlertPanelProps {
|
||||
rule: Rule;
|
||||
|
@ -16,10 +18,6 @@ const alertColors: RuleStatus<string> = {
|
|||
inactive: 'success',
|
||||
};
|
||||
|
||||
const createExpressionLink = (expr: string) => {
|
||||
return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.range_input=1h`;
|
||||
};
|
||||
|
||||
const CollapsibleAlertPanel: FC<CollapsibleAlertPanelProps> = ({ rule, showAnnotations }) => {
|
||||
const [open, toggle] = useState(false);
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import React, { FC } from 'react';
|
||||
import { RouteComponentProps } from '@reach/router';
|
||||
import PathPrefixProps from '../../types/PathPrefixProps';
|
||||
import { Alert } from 'reactstrap';
|
||||
import { useFetch } from '../../hooks/useFetch';
|
||||
import { withStatusIndicator } from '../../components/withStatusIndicator';
|
||||
import { RulesMap, RulesContent } from './RulesContent';
|
||||
|
||||
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => (
|
||||
<>
|
||||
<h2>Rules</h2>
|
||||
<Alert color="warning">
|
||||
This page is still under construction. Please try it in the <a href={`${pathPrefix}/rules`}>Classic UI</a>.
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
const RulesWithStatusIndicator = withStatusIndicator(RulesContent);
|
||||
|
||||
const Rules: FC<RouteComponentProps & PathPrefixProps> = ({ pathPrefix }) => {
|
||||
const { response, error, isLoading } = useFetch<RulesMap>(`${pathPrefix}/api/v1/rules`);
|
||||
|
||||
return <RulesWithStatusIndicator response={response} error={error} isLoading={isLoading} />;
|
||||
};
|
||||
|
||||
export default Rules;
|
||||
|
|
131
web/ui/react-app/src/pages/rules/RulesContent.tsx
Normal file
131
web/ui/react-app/src/pages/rules/RulesContent.tsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
import React, { FC } 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 { now } from 'moment';
|
||||
|
||||
interface RulesContentProps {
|
||||
response: APIResponse<RulesMap>;
|
||||
}
|
||||
|
||||
interface RuleGroup {
|
||||
name: string;
|
||||
file: string;
|
||||
rules: Rule[];
|
||||
evaluationTime: string;
|
||||
lastEvaluation: string;
|
||||
}
|
||||
|
||||
export interface RulesMap {
|
||||
groups: RuleGroup[];
|
||||
}
|
||||
|
||||
const GraphExpressionLink: FC<{ expr: string; title: string }> = props => {
|
||||
return (
|
||||
<>
|
||||
<strong>{props.title}:</strong>
|
||||
<Link className="ml-4" to={createExpressionLink(props.expr)}>
|
||||
{props.expr}
|
||||
</Link>
|
||||
<br />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
{groups.map((g, i) => {
|
||||
return (
|
||||
<Table bordered 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 style={{ backgroundColor: '#F5F5F5' }} 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 style={{ backgroundColor: '#F5F5F5' }}>
|
||||
<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>
|
||||
</Table>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import { Alert, RuleState } from '../pages/alerts/AlertContents';
|
||||
|
||||
export interface Metric {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
@ -7,3 +9,18 @@ export interface QueryParams {
|
|||
endTime: number;
|
||||
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;
|
||||
name: string;
|
||||
query: string;
|
||||
state: RuleState;
|
||||
type: string;
|
||||
}
|
||||
|
|
|
@ -197,3 +197,7 @@ export const toQueryString = ({ key, options }: PanelMeta) => {
|
|||
export const encodePanelOptionsToQueryString = (panels: PanelMeta[]) => {
|
||||
return `?${panels.map(toQueryString).join('&')}`;
|
||||
};
|
||||
|
||||
export const createExpressionLink = (expr: string) => {
|
||||
return `../graph?g0.expr=${encodeURIComponent(expr)}&g0.tab=1&g0.stacked=0&g0.range_input=1h`;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue