Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,7 @@ module github.com/flashcatcloud/flashduty-cli
go 1.25.1

require (
github.com/flashcatcloud/go-flashduty v0.14.1
github.com/flashcatcloud/go-flashduty v0.14.2
github.com/mattn/go-runewidth v0.0.27
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,6 +3,8 @@ github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/flashcatcloud/go-flashduty v0.14.1 h1:yrpKPuMJuC1z8bhIaZfGV66lb+sAsBUcqOZZsFZjsiI=
github.com/flashcatcloud/go-flashduty v0.14.1/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
github.com/flashcatcloud/go-flashduty v0.14.2 h1:SIF8AMoaF67eEGAiiq8e4y5Dvv/0OOA/VzoxYpDB1CE=
github.com/flashcatcloud/go-flashduty v0.14.2/go.mod h1:aA0RtZEs0AYOwwdNKdtVeD8YMOdnmVY1zAlVD+9Ovx8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
Expand Down
52 changes: 50 additions & 2 deletions skills/flashduty/reference/alert.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -146,9 +146,57 @@ View alert timeline

`pipeline-upsert` replaces the whole pipeline (max 50 rules); `rules[].kind` values: `title_reset` · `description_reset` · `severity_reset` · `alert_drop` · `alert_inhibit`. The `rules` array has no typed flag — pass it via `--data '{"rules":[...]}'`. The call is idempotent (upsert), so re-running with the same body is safe.

`settings` shape depends on `kind`: `title_reset` → `{"title": "<template>"}`; `description_reset` → `{"description": "<template>"}`; `severity_reset` → `{"severity": "Critical"|"Warning"|"Info"}`; `alert_drop` → `{}` (empty object); `alert_inhibit` → `{"equals": ["<label_key>", ...], "source_filters": <OrFilterGroup>}`.
`settings` shape depends on `kind`: `title_reset` → `{"title": "<template>"}`; `description_reset` → `{"description": "<template>"}`; `severity_reset` → `{"severity": "Critical"|"Warning"|"Info"}`; `alert_drop` → `{}` (empty object); `alert_inhibit` → `{"equals": ["<label_key>", ...], "source_filters": [<condition>, ...]}`.

**`rules[].if` (and `alert_inhibit`'s `source_filters`) are OR-of-AND condition trees** — read `reference/filters.md` before composing them. Note this differs from `enrichment`'s rule-level `if`, which is a single flat AND-only condition list.
**`rules[].if` and `alert_inhibit.source_filters` are FLAT AND-lists** — one array of `{key, oper, vals}` conditions, ALL of which must match:

```json
"if": [{"key": "labels.env", "oper": "IN", "vals": ["prod"]}]
```

This is the same shape as `enrichment`'s rule-level `if`, and it is **not** the
OR-of-AND tree used by silence / inhibit / drop / escalation rules. Wrapping the
conditions in a second array is rejected before the request leaves the CLI
(`cannot unmarshal array into ... of type FilterCondition`). Read
`reference/filters.md` for the operators, the missing-key trap, and the key
vocabulary — those all apply here; only the nesting differs.

## Writing `title_reset` / `description_reset` templates

The `settings.title` and `settings.description` values are NOT plain strings —
they are rendered, and the two kinds render differently:

| | with a leading `[TPL]` | without it |
|---|---|---|
| `title_reset` | rendered as a template | treated as a `::`-joined key list (below) |
| `description_reset` | rendered as a template | **silently does nothing** — the rule is a no-op |

**Always write `[TPL]` for `description_reset`.** Omitting it is not an error;
the rule simply never fires, which reads as "the pipeline didn't apply".

Inside a `[TPL]` value, two substitutions run in order:

1. `${label_name}` — replaced with that label's value; a missing label renders
`<no value>` rather than failing.
2. Go `text/template` — the event is the dot, so labels are
`{{.Labels.<name>}}` (`Labels` capitalised; it is a map).

```json
{"kind": "title_reset", "settings": {"title": "[TPL]{{.Labels.service}} / {{.Labels.check}}"}}
{"kind": "description_reset", "settings": {"description": "[TPL]${instance} is late by ${seconds}s"}}
```

**The bare `::` form (title only).** Without `[TPL]`, the title is split on
`::`; each segment is either a literal or `$name`, which resolves to
`labels.<name>`. Segments are joined with position-fixed separators — nothing,
then ` / `, then ` - `, then ` ⋅ ` for the rest — so `$service::$check` renders
as `payments-api / disk_used`. Prefer the `[TPL]` form: it is explicit about
where values come from and does not hard-code separators.

**Labels the template reads must already exist.** Enrichment runs BEFORE the
pipeline, so labels produced by `fduty enrichment upsert` (extraction,
composition, mapping) are available here — but a label produced by a LATER
pipeline rule is not, and a typo just renders `<no value>` into the title.

## Gotchas

Expand Down
9 changes: 8 additions & 1 deletion skills/flashduty/reference/filters.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,12 @@ the rule fires when ANY group matches; each inner array holds

→ (Critical AND service=payments-api) OR (env=staging).

**One exception — alert pipeline.** `rules[].if` and
`alert_inhibit.source_filters` (`reference/alert.md`) take a FLAT array of
conditions, all AND-ed, with no outer grouping array. Same operators, same
keys, one less level of nesting. Everything below applies to them except the
shape.

## Operators and values

- `oper` is `IN` (the object's value for `key` must equal one of `vals`) or
Expand All@@ -42,7 +48,8 @@ custom label. Per family:

| rule family | matched against | extra keys | keys that DO NOT exist here |
|---|---|---|---|
| silence / drop (`filters`), inhibit (`source_filters` / `target_filters`), alert pipeline (`rules[].if`, `alert_inhibit.source_filters`) | each alert event | `alert_key`, `title_rule` | `dedup_key` |
| silence / drop (`filters`), inhibit (`source_filters` / `target_filters`) | each alert event | `alert_key`, `title_rule` | `dedup_key` |
| alert pipeline (`rules[].if`, `alert_inhibit.source_filters`) — **flat AND-list**, see above | each alert event | `alert_key`, `title_rule` | `dedup_key` |
| escalation (`filters`) | the incident | `dedup_key` | `alert_key`, `title_rule` |

A key outside the family's vocabulary (e.g. `dedup_key` in a silence rule)
Expand Down