Skip to content
KONIGI

Dashboards / Product mechanics / Alert rule attached to panel

1 of 10

Alert rule attached to panel

The chart shows the line to watch; the viewer wants to be told when it's crossed.

Updated September 10, 2026

Problem

Someone is looking at a chart and realises they cannot watch it forever. The thing they want is for the chart to watch itself, and the moment of wanting that is while looking at the chart, not while sitting in an alerting console three menus away.

Solution

Let a rule be created from the panel, carrying the query with it. The line you just drew becomes the condition, and the panel and the alert stay describing the same thing.

The proximity is the point. An alert defined elsewhere drifts from the chart within a quarter, and then two numbers exist: the one on the wall and the one that pages someone. Attaching the rule to the panel makes them one object, which is the only reliable way to keep them equal.

What the pattern must not do is make alerting easy enough to be thoughtless, and here the Google SRE book is the correction worth carrying into the UI. Every page should be actionable. Every page response should require intelligence—if a page merely merits a robotic response, it shouldn’t be a page. And catching symptoms is worth much more effort than catching causes, with causes reserved for the very definite and very imminent.

A one-click “alert on this panel” button is at odds with all three, because the easiest rule to create is a static threshold on a cause-shaped metric like CPU, which is exactly the page that wakes someone for something no user noticed. The interface should make the useful rule as easy as the tempting one: ask for duration as well as threshold, ask what happens on no data, and ask who it goes to.

That no-data question is the one products get wrong most often. Grafana treats it as a first-class decision with four mappings—Set No Data state, Set Alerting state, Set Normal state, Keep last state—and defaults to creating a DatasourceNoData instance rather than quietly calling silence healthy. A rule that cannot distinguish “the metric is fine” from “the metric stopped arriving” is a rule that will be silent during exactly the outage it was written for.

Use when

The metric has a defensible limit, someone owns the response, and the chart is where people already look. Best where the panel and the rule genuinely describe one thing.

Don’t use when

Nobody would act on it. An alert nobody can respond to trains a team to ignore the channel, and the cost lands on the next alert rather than this one. Also avoid it when the condition needs several signals: a per-panel rule encourages single-metric thinking.

Trade-offs

Panel-bound rules scale badly. Two hundred panels means two hundred rules with two hundred owners, each edited by whoever last touched the dashboard. Duplication follows, because the same condition gets attached to the same metric on four dashboards, and now one incident pages four times. The rule’s lifecycle also gets tied to the panel’s: delete or restructure a dashboard and the alerting goes with it, usually unnoticed. And a static threshold on a chart that autoscales looks reasonable while the traffic pattern quietly outgrows it.

Checklist

  • Is this symptom or cause, and if cause, is it definite and imminent?
  • Could a person act on this at 3am, and would the action be more than restarting something?
  • Is there a duration condition, or does a single spike page?
  • What happens on no data, and was that chosen deliberately?
  • Does the rule use the same query as the panel, or a copy that can drift?
  • Who owns this rule, and do they know?
  • Does the same condition exist on another dashboard?
  • What happens to the rule if the panel or dashboard is deleted?
  • Is the threshold still right for current traffic, and when was that checked?
  • How often has this fired, and how often did it lead to action?

Compare

Grafana creates alert rules from panels and forces the no-data decision at rule-creation time with four explicit mappings, which is the most honest treatment of the failure mode most products leave implicit. Prometheus keeps rules in version-controlled config rather than in a UI, so they get reviewed, diffed and owned like code, at the cost of the create-it-while-looking-at-it immediacy this pattern is named for. Datadog makes the monitor the primary object and lets dashboards reference it, which inverts the relationship and stops the same condition being redefined per dashboard. Sentry derives alerting from issue behaviour—new, regressed, spiking—rather than from a threshold on a line, which sidesteps threshold staleness entirely and only works because its data has a natural notion of “new”.

Threshold line is the visual half of this pattern and should be drawn from the same number. Metric targets is where the limit ought to be defined and owned. Error and stale state covers what the panel shows when the same condition that should alert has instead produced silence. Semantic status color is how the fired state reads on the page. Status history is the record of what this rule has actually done.

Alert rule attached to panel anatomy A panel with a threshold drawn on it and the rule created from it, carrying the same query. The rule form asks for a duration as well as a value, asks what to do when no data arrives, and asks who it goes to. The line you drew becomes the condition Checkout error rate 1% New rule condition error rate > 1% for 5 minutes on no data alerting, not normal routes to payments on-call 1 2 3 1 ONE OBJECT, NOT TWO A rule defined elsewhere drifts from the chart inside a quarter, and then there are two numbers: the one on the wall and the one that wakes somebody. 2 DURATION, NOT JUST VALUE The easiest rule to create is a static threshold on a cause-shaped metric, which is exactly the page nobody needed. Make the useful rule as easy as the tempting one. 3 WHAT NO DATA MEANS A rule that can't tell "the metric is fine" from "the metric stopped arriving" will be silent during exactly the outage it was written for. Every page should be actionable. If it merits a robotic response, it shouldn't be a page.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

The chart shows the line to watch and the rule lives on it. Drawing the threshold on the same axes as the data is the only way a reader can see how close the current value is to firing.

shadcn
npx shadcn@latest add card input select
npm
recharts
Tokens
--card--card-foreground--muted-foreground--border--status-critical--status-warn--chart-1

Checkout error rate

1%

New rule

conditionerror rate > 1%for5 minuteson no dataroutes topayments on-call

AlertRule.tsxThe condition is computed from the panel, the threshold is drawn on the panel's axes, and no-data is a closed choice with four values.

import { Line, LineChart, ReferenceLine, XAxis, YAxis } from "recharts";
import { Card } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";

/**
 * The rule is created from the panel and carries the panel's query, so the
 * line on the chart and the condition that pages someone are one object.
 * Nothing here lets the two be typed separately.
 */
export type Panel = {
  title: string;
  /** The metric as the rule will name it: "error rate", "p95 latency". */
  metric: string;
  unit: string;
  series: number[];
};

/** Grafana's four. A rule that cannot say what silence means is silent during
 *  the outage it was written for, so this is a required choice, not a default. */
export type NoData = "alerting" | "normal" | "no-data" | "keep-last";
export const NO_DATA_LABEL: Record<NoData, string> = {
  alerting: "alerting, not normal",
  normal: "normal",
  "no-data": "no data",
  "keep-last": "keep last state",
};

export type Rule = {
  threshold: number;
  /** Minutes the condition must hold. A single spike is not a page. */
  forMinutes: number;
  noData: NoData;
  routeTo: string;
};

/** "error rate > 1%": computed from the panel, never typed into the rule. */
export const condition = (panel: Panel, rule: Rule) => `${panel.metric} > ${rule.threshold}${panel.unit}`;

const CRITICAL = "hsl(var(--status-critical))";

export function AlertRule({ panel, rule, onChange, width = 280, height = 150 }: {
  panel: Panel;
  rule: Rule;
  onChange: (rule: Rule) => void;
  width?: number;
  height?: number;
}) {
  const data = panel.series.map((value, i) => ({ i, value }));
  const top = Math.max(rule.threshold, ...panel.series) * 1.15;
  const field = "h-7 w-24 text-right text-xs tabular-nums";
  return (
    <div className="grid gap-5 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.15fr)]">
      <Card className="p-4">
        <p className="border-b pb-2 text-xs uppercase tracking-wide text-muted-foreground">{panel.title}</p>
        <div className="mt-3 overflow-x-auto">
          <LineChart width={width} height={height} data={data} margin={{ top: 8, right: 8, bottom: 0, left: 8 }}>
            <XAxis interval={0} dataKey="i" hide />
            <YAxis domain={[0, top]} hide />
            {/* The threshold on the data's own axes. This is the whole reason
                the rule lives on the panel: the reader can see how close it is. */}
            <ReferenceLine y={rule.threshold} stroke={CRITICAL} strokeWidth={2} strokeDasharray="5 4"
              label={{ value: `${rule.threshold}${panel.unit}`, position: "insideTopRight", fill: CRITICAL, fontSize: 10 }} />
            <Line type="linear" dataKey="value" stroke="hsl(var(--chart-1))" strokeWidth={2} dot={false} isAnimationActive={false} />
          </LineChart>
        </div>
      </Card>

      <Card className="p-4">
        <p className="border-b pb-2 text-xs uppercase tracking-wide text-muted-foreground">New rule</p>
        <div className="mt-3 grid grid-cols-[84px_1fr] items-center gap-x-3 gap-y-2.5 text-xs">
          <span className="text-muted-foreground">condition</span>
          <span className="flex items-center justify-end gap-2 font-mono text-card-foreground">
            {condition(panel, rule)}
            <Input type="number" step="0.1" value={rule.threshold} aria-label={`threshold in ${panel.unit}`} className={field}
              onChange={(e) => onChange({ ...rule, threshold: Number(e.target.value) })} />
          </span>

          <span className="text-muted-foreground">for</span>
          <span className="flex items-center justify-end gap-2 tabular-nums text-card-foreground">
            {rule.forMinutes} minutes
            <Input type="number" min={1} value={rule.forMinutes} aria-label="minutes the condition must hold" className={field}
              onChange={(e) => onChange({ ...rule, forMinutes: Number(e.target.value) })} />
          </span>

          <span className="text-muted-foreground">on no data</span>
          <span className="flex justify-end">
            <Select value={rule.noData} onValueChange={(v) => onChange({ ...rule, noData: v as NoData })}>
              <SelectTrigger aria-label="what no data means"
                className={`h-7 w-auto gap-2 text-xs ${rule.noData === "alerting" ? "text-status-warn" : "text-card-foreground"}`}>
                <SelectValue>{NO_DATA_LABEL[rule.noData]}</SelectValue>
              </SelectTrigger>
              <SelectContent>
                {(Object.keys(NO_DATA_LABEL) as NoData[]).map((k) => (
                  <SelectItem key={k} value={k} className="text-xs">{NO_DATA_LABEL[k]}</SelectItem>
                ))}
              </SelectContent>
            </Select>
          </span>

          <span className="text-muted-foreground">routes to</span>
          <span className="text-right text-card-foreground">{rule.routeTo}</span>
        </div>
      </Card>
    </div>
  );
}

demo.tsxHow it is called: the panel's series and a rule at 1% for 5 minutes, alerting on no data.

import { useState } from "react";
import { AlertRule, type Rule } from "./AlertRule";

/**
 * The checkout error rate over the last eight samples, crossing 1% twice.
 * Changing the threshold moves the line on the chart, because the rule and
 * the panel are the same object.
 */
const PANEL = {
  title: "Checkout error rate",
  metric: "error rate",
  unit: "%",
  series: [0.24, 0.33, 0.27, 0.48, 1.12, 1.21, 0.61, 0.7],
};

export default function Demo() {
  const [rule, setRule] = useState<Rule>({
    threshold: 1,
    forMinutes: 5,
    noData: "alerting",
    routeTo: "payments on-call",
  });
  return <AlertRule panel={PANEL} rule={rule} onChange={setRule} />;
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

Captures whose hotspots reference this pattern, grouped by product and dated. The dashed boxes are this pattern; hover any box for the note.

Grafana

The reference implementation for panel grids, template variables, and stat panels; most other tools are defined by how they differ from it.

Examples / Alert List September 10, 2026 Grafana Play (signed out; no version string exposed) medium · dark · desktop-web
Seven alerts, and the durations are the story. "Browser market share > 50%" has been firing for 17 days, 15 hours and 12 minutes. Two more have been firing for over three days. An alert that has been true for two and a half weeks is not telling anyone anything—it has become part of the background, and the list it sits in is now a list you scroll past. The other thing worth reading is the names. Six alerts, six conventions: a full sentence, a metric identifier in snake case, a camel-case service name, a two-word phrase, and two that just say "Dynamic". Somebody arriving at this list at three in the morning cannot tell from the names what is broken or how badly, which is work the naming could have done for free.
  • Alert rule attached to panel Seven rules in six naming conventions. Nothing in the list conveys severity or subject.
  • Alert rule attached to panel Firing for 17 days. Still actionable, in principle; nobody has acted for two and a half weeks.
  • Semantic status color Pending rather than firing: the threshold is met and the duration isn't. Two states, both named.
  • Filter bar "1 instance, 24 hidden by filters"—the list saying what it is not showing you.
  • Drill-down The route from an alert back to the rule that defined it, on every row.