External Annotations
Grafatui can overlay read-only, external point events from exactly one source:
a JSONL file or a command provider. It never edits or writes either source.
External annotations are deliberately separate from Grafana dashboard
annotations: Grafatui does not implement Grafana annotation queries, APIs,
annotations, or annotations.list.
Enable Annotations
Select exactly one source. For a file source, pass the path on the command line:
grafatui \
--grafana-json ./dashboard.json \
--annotations-file ./events.jsonl
Or configure the file source in TOML:
annotations_file = "./events.jsonl"
For a command source, configure an executable that accepts the request protocol below. The command receives no shell interpolation:
[annotations_command]
program = "./target/debug/examples/git_annotation_provider"
args = ["."]
timeout = "10s"
Or select it from the command line:
grafatui \
--grafana-json ./dashboard.json \
--annotations-command ./target/debug/examples/git_annotation_provider \
--annotations-command-arg=.
File and command sources are mutually exclusive. A TOML configuration that sets both is rejected even if the CLI selects a source. A CLI file or command replaces the complete TOML annotation source; it never mixes a CLI program, arguments, or timeout with TOML values. Sources are opt-in and read-only; Grafatui does not create, edit, or otherwise write them.
Command Provider Protocol
Grafatui writes exactly one version-1 request line to the command’s standard input, then closes stdin. The request defines the complete refresh window:
{"version":1,"range":{"from":"2026-08-12T10:00:00Z","to":"2026-08-12T10:05:00Z"}}
range.from and range.to are inclusive UTC RFC 3339 bounds. Grafatui
defensively applies its visible-range filtering to the events returned.
The provider writes zero or more existing JSONL events to stdout and diagnostics
to stderr. Exit 0 with valid bounded JSONL replaces the complete annotation
snapshot; an empty successful stdout clears it. A spawn failure, timeout,
nonzero exit, invalid UTF-8 or JSONL, or oversized stdout keeps the last valid
snapshot and shows a warning.
The default timeout is 10 seconds. Grafatui accepts at most 10 MiB of stdout and captures at most 64 KiB of stderr. Providers inherit Grafatui’s current directory and environment. Put credentials in that environment or use standard credential tooling; never place secrets in dashboard JSON or command arguments.
The included Git provider is a practical starting point:
cargo build --example git_annotation_provider
printf '%s\n' '{"version":1,"range":{"from":"2026-08-12T10:00:00Z","to":"2026-08-12T10:05:00Z"}}' \
| ./target/debug/examples/git_annotation_provider .
JSONL Event Format and Targeting
The file contains one JSON object per line. Blank lines are ignored. Each event
requires time to be an RFC3339 string with an explicit timezone or offset
(numeric timestamps are rejected) and non-empty text. tags is an optional
array of non-empty strings.
{"time":"2026-07-23T14:30:00Z","text":"Maintenance window","tags":["maintenance"]}
{"time":"2026-07-23T14:30:00Z","text":"Deployed v2.4","tags":["deploy","production"],"panel_titles":["HTTP Request Rate by Status Code"]}
Omit panel_titles to target all eligible graph and timeseries panels, as in
the first event. When panel_titles is present, it must contain one or more
non-blank titles and each title is matched exactly and case-sensitively against
eligible graph/timeseries panel titles. null, an empty array, and blank
titles are validation errors.
If a title occurs on multiple eligible panels, the event fans out to all of them and Grafatui shows one warning for that duplicate title. A title that is missing, or exists only on a non-graph panel, shows one warning and renders no marker for that title. These titles are Grafatui routing labels, not Grafana panel IDs.
Events are ordered by timestamp. Unknown JSON fields are ignored. Times with fractional seconds are accepted, and the full fractional timestamp is used when projecting an event onto the graph even when the space-limited inline timestamp display shows less precision.
Target, Filter, Inspect, and Reload
This walkthrough uses current UTC timestamps so both events fall in the visible
15-minute range. It uses only POSIX shell tools; no jq is required.
First, start the bundled Prometheus demo stack from the repository root:
cd examples/demo && docker-compose up -d && sleep 5 && cd ../..
Then create the annotation file and run Grafatui:
annotation_demo_file=/tmp/grafatui-annotations-demo.jsonl
annotation_demo_time="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
printf '{"time":"%s","text":"Maintenance window","tags":["maintenance"]}\n' \
"$annotation_demo_time" > "$annotation_demo_file"
printf '{"time":"%s","text":"API deployed","tags":["deploy","production"],"panel_titles":["HTTP Request Rate by Status Code"]}\n' \
"$annotation_demo_time" >> "$annotation_demo_file"
cargo run -- \
--grafana-json examples/dashboards/prometheus_demo.json \
--prometheus-url http://localhost:19090 \
--range 15m \
--annotations-file "$annotation_demo_file"
Maintenance window appears on every graph/timeseries panel. API deployed
appears only on HTTP Request Rate by Status Code. Press t, select deploy
with Space, and press Enter; only the targeted deployment remains. Press
v, move the cursor to the marker, and press Enter; the selected panel’s
cluster list and selected-event detail pane open.
While Grafatui is running, append an event in a second terminal:
annotation_demo_file=/tmp/grafatui-annotations-demo.jsonl
annotation_demo_time="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
printf '{"time":"%s","text":"Rollback started","tags":["rollback","production"],"panel_titles":["HTTP Request Rate by Status Code"]}\n' \
"$annotation_demo_time" >> "$annotation_demo_file"
The new event is loaded after the normal refresh; Grafatui does not need to
restart. With only deploy selected, Rollback started remains hidden. Press
t, then c, and press Enter to apply the cleared filter and reveal the
rollback marker. Alternatively, select rollback in the filter and apply it.
Tag Filter and Cluster Controls
Press t to open the global annotation tag filter. It is runtime-only: it is
not written to the JSONL source or configuration, and applies to every eligible
panel. Selected tags use exact, case-sensitive OR matching: an event remains
visible when it has any selected tag. With no selected tags, events with and
without tags remain visible. The catalogue keeps a selected tag with a zero
event count so it can be removed after a reload.
In the tag filter, use Up/Down or j/k to move, Space to toggle the
highlighted tag, c to clear the draft, Enter to apply it, or Esc to
cancel without changing the applied filter. In inspection mode, Enter opens
only the cluster actually rendered by the selected panel at the cursor. In the
cluster, use Up/Down or j/k to select an event, PgUp/PgDn to page,
and Enter or Esc to close. Mouse input is ignored while either annotation
modal is open.
Cluster contents are frozen when the cluster opens: a later reload can replace the live annotation snapshot without moving rows or changing the cluster’s event details.
Automatic Reload, Rendering, and Exports
Grafatui refreshes both source types during each normal refresh, including while markers are hidden. It checks a file source’s metadata and, when it changes, reads, parses, and validates the full candidate file before atomically replacing the snapshot. A zero-byte file is a valid update that clears all events. Command and Prometheus refreshes share the same time range, start together, and redraw together. Annotation loading is independent of Prometheus: annotation failures never fail startup or a Prometheus refresh.
Only graph and timeseries panels receive annotation markers. Press a to
toggle marker visibility. Events that project to the same terminal column are
shown as one counted marker: • for one event, decimal 2–9 for two through
nine events, and + for 10 or more. In inspection mode, moving the cursor onto
that column shows the cluster’s timestamp, text, and tags inline; multi-event
details report the exact cluster count.
Applied panel targeting and tag filtering affect the markers in SVG/PNG exports and changed-frame recordings. Active inline annotation details are exportable; the tag-filter and cluster modal chrome is not.
Errors and Last Valid Snapshot
If a file is missing, unreadable, or contains a malformed event, Grafatui keeps rendering the last valid snapshot and shows an annotation warning. Command provider failures follow the same rule. A bad update does not replace the previously loaded events, fail startup, or fail the Prometheus refresh.
CI/CD and Provider Integrations
CI workflow → durable deployment/release record → command provider query
→ normalized JSONL point events → Grafatui overlay
GitHub Actions is a useful concrete pattern: let a workflow record deployment, release, or workflow outcomes in an API, object store, database, or shared event log. A local provider receives Grafatui’s requested range and queries that system of record, then emits normalized JSONL point events. Useful tags include repository, workflow, environment, status, commit, and deployment.
Give the provider credentials through its environment or standard credential tooling, never dashboard JSON or command arguments. A shared JSONL file is a reasonable source only when the workflow and Grafatui genuinely share storage; do not commit an ever-growing event log to the application repository. Vendor-specific providers should normally live as user or community plugins. Built-in integrations remain demand-driven.
Current Limits and Roadmap
This feature supports one external file or command source, point events,
panel-title routing, and one global runtime-only tag filter. It has no stable
event IDs, no per-panel tag filters, no multiple sources, and no editing. It
does not add Grafana annotation-query/API compatibility. Range events, stable
event IDs, and per-panel tag filters remain deferred. --validate validates
Grafana dashboard imports only; it does not validate annotations.