Skip to content

feat: alerting kinds, status gauge, grafonnet dashboards, oteldemo kind harness - #11

Merged
thisisibrahimd merged 44 commits into
mainfrom
demo-slos
Sep 8, 2026
Merged

thisisibrahimd merged 44 commits into
mainfrom
demo-slos

Conversation

@thisisibrahimd

Copy link
Copy Markdown
Owner

Summary

Releases the v0.2.0 milestone: extends the Prometheus generator with four
alerting kinds and a status gauge, ships a grafonnet-based dashboard
provider plus spec-integrity recording rules, adds a kind-cluster harness
around the OpenTelemetry Astronomy Shop demo, and replaces the docker-compose
harness in examples/oteldemo/. Documentation, CHANGELOG, and AGENTS.md
are updated to match. CI workflows are SHA-pinned for supply-chain
hardening.

44 commits ahead of main, working tree clean.

Breaking changes

  • Spec loading now fails loud on decode / ref-resolution errors
    (pkg/specstore/specstore.go). Non-OpenSLO YAML is no longer silently
    dropped — invalid specs surface during load / validate / generate.
  • Prometheus generator drops openslo_slo_metadata ratio series when a
    ratio SLI is present. Ratio SLIs additionally emit
    openslo_sli_event_rate_<window> recording series counted from the
    metric source marked isGood.
  • Recording rule labels no longer include openslo_slo_metadata_* (folded
    into the recording rules under openslo_slo_*); dashboards and queries
    that referenced these labels must move to openslo_slo_name /
    openslo_slo_description.
  • The severity label is now suffixed (critical, warning, info) — the
    bare label is removed.

Generator

  • Four alerting kinds: burn-rate, tiered, time-window, standard.
  • Status gauge records an SLI status boolean (good / bad) keyed by
    openslo_slo_name, plus the SLO description via
    openslo_slo_description (semconv openslo.slo.description).
  • Ratio SLI precedence prefers the spec metric-source definition over
    per-metric defaults; threshold cases rule out unreliable ratio math.
  • pkg/semconv/semconv_gen.go regenerated; new openslo.alert.* attributes
    wired into the templates.

Dashboards

  • New grafonnet source under deploy/mixins/ driving a Grafana provisioning
    provider (openslo-dashboards.yaml) — JSON leaves the repo (deterministic
    rebuild via make dashboards).
  • openslo-list + openslo-detail dashboards published; filterable by SLO
    name, description, slo-criticality.
  • deploy/rules/openslo-integrity-rules.yaml ships an
    OpenSloSpecDrift recording rule + alert that detects when the
    deployed OpenSLO spec set drifts from the Prometheus recording rules
    in openslo_recording_rules_hash.

oteldemo kind harness

  • Replaces docker-compose: examples/oteldemo/kind/setup.sh|teardown.sh|sync.sh
    • values.yaml install the upstream helm chart in a single-node kind
      cluster.
  • 11 SLOs in examples/oteldemo/specs/ (ad / cart / checkout / email /
    frontend / image / kafka / payment / product-catalog / recommendation /
    etc.) covering availability, latency, and ratio indicators.
  • Generated rules in examples/oteldemo/rules/ (one YAML per SLO, 30 rules
    each); validated via promtool check rules.
  • New make start-demo|stop-demo|sync|verify|lint-rules|clean|generate
    targets documented in examples/oteldemo/Makefile + README.

Examples

  • examples/multi-dim-slo/ ships a sample multi-dimensional SLO bundle
    driven by multi-dimensional-sli.openslo.com/dimensions /
    multi-dimensional-sli.openslo.com/label annotations (feature-flagged).

Docs

  • README.md rewritten with a 9-section TOC; covers install, CLI surface,
    generator, dashboards, and the oteldemo harness end-to-end.
  • AGENTS.md updated with the new example layout, semconv-codegen
    workflow, SDK API gotchas, and the SLI source conventions (Path Y / Y-fauna).
  • CHANGELOG.md v0.2.0 with full Added / Changed / Fixed / Removed /
    Deprecated / Dashboards subsections.

CI / hardening

  • .github/workflows/build-container.yaml SHA-pinned to upstream actions
    (actions/checkout, actions/setup-go, docker/setup-qemu-action,
    docker/setup-buildx-action, docker/login-action,
    docker/build-push-action); immutable digest references, no mutable tags.

Verification

  • make build clean; make lint, make test, make tidy pass locally.
  • Generator snapshots under
    internal/generator/prometheusgenerator/testdata/ regenerated alongside
    template changes; single-line, multi-line, ratio, multi-dim, tiered
    cases all green.
  • Golden tests: go test ./internal/generator/prometheusgenerator/....
  • make verify + make generate + make lint-rules green in
    examples/oteldemo.

Post-merge

  • Tag v0.2.0 once main carries the merge commit (git tag v0.2.0 +
    git push --tags); GoReleaser publishes with prerelease: auto per
    repo config.
  • Provisioning Grafana with deploy/dashboards/openslo-dashboards.yaml
    is a follow-up — out of scope for the PR.

- github.com/sebdah/goldie/v2 for golden-file based snapshot tests in
  the Prometheus generator test suite.
- promtool tool pinned via mise for local validation of generated
  Prometheus rules.
Extends the OpenTelemetry Weaver registry with three new SLI/SLO metrics
the Prometheus generator now produces:

- openslo.slo.current_burn_rate — instantaneous 5m burn rate / budget
- openslo.slo.period_burn_rate — full-window burn rate over the SLO period
- openslo.slo.period_error_budget_remaining — 1 minus period_burn_rate

The generated Go constants in pkg/semconv/semconv_gen.go are refreshed
to match the registry.
The OpenSLO SDK models only a single AlertCondition kind ("burnrate").
opensloctl now accepts the four kebab-case names that map one-to-one
onto the Google SRE Workbook alerting strategies:

- error-rate                  (workbook §§ 1–3)
- burn-rate                   (workbook § 4)
- multi-burn-rate             (workbook § 5)
- multi-window-multi-burn-rate (workbook § 6)

Per-kind threshold range checks are enforced:

- error-rate:           threshold in (0, 1]
- burn-rate families:   threshold > 0

The SDK's strict AlertConditionKind OneOf is bypassed for AlertCondition
specs only — every other SDK validation still runs. Duplicate detection,
rolling-window requirement, and target/targetPercent required rules are
preserved.
…ecycle

Replaces the placeholder loader_test.go with a focused specstore_test.go
exercise covering the full store/validate pipeline:

- AlertConditionKind membership (closed set of four kinds)
- Threshold range checks per kind (error-rate vs burn-rate families)
- Cross-ref resolution for every arc in the SLO graph
  (SLO→Service/SLI/AlertPolicy, AlertPolicy→Condition/Target,
   SLI→DataSource)
- SLI metric source validation
- Duplicate-name detection across all spec kinds
- Target/targetPercent required and isRolling: true required post-checks

A small internal/testutil helper package is added to construct minimal
SLO/SLI/AlertCondition specs inline so tests stay declarative.
The Prometheus generator previously rendered two separate files per SLO
(-recording-rules.yaml and -alert-rules.yaml) using two templates. This
collapses both into a single unified template that emits a single
<slo-name>-rules.yaml file containing:

- SLO info recording rules (info, objective, timewindow_days, error_budget,
  current_burn_rate, period_burn_rate, period_error_budget_remaining)
- Windowed SLI error rate recordings (5m, 30m, 1h, …)
- An optional openslo-alerts-<slo> group when AlertPolicies are referenced

The AlertCondition kind drives both the expression shape and the tier
structure:

- error-rate:                   1 condition per severity
- burn-rate:                    1 condition per severity
- multi-burn-rate:              >=2 conditions OR-ed per severity
- multi-window-multi-burn-rate: AND within tier, OR across tiers, derived
  by stripping the trailing "-<lookbackWindow>" suffix from condition names

Alert names follow {SloNamePascal}{KindPascal} (PascalCase, no
underscores, severity stays in the label). The legacy "burnrate" kind
maps to multi-window-multi-burn-rate with a one-shot warning.

The standalone prometheus-alert-rules.template.yaml is removed; all alert
rendering flows through the unified template.
…s labels

The promLabelsFromOpenSlo helper that maps OpenSLO metadata.labels into
Prometheus label key/value pairs now enforces two rules:

- Label names with hyphens are rejected — Prometheus requires
  [a-zA-Z_][a-zA-Z0-9_]* (snake_case), and silently rewriting the user's
  key would have hidden config typos.
- Multi-value labels (more than one entry per key) are rejected — only a
  single value per label key is supported; the SDK normalizes the
  single-string YAML form to a one-element slice which is accepted.

The feature package's existing multi-dimensional SLI annotations keep
working unchanged; their godoc is tidied up.
The PrometheusGenerator already stopped short of two loading-time
checks; both are now wired end-to-end:

- spec.indicatorRef is resolved into an inline v1.SLOIndicatorInline at
  generation time. Inline spec.indicator still wins when both are set;
  the resolver returns a clear error if neither is present.
- specstore.GetSpecs validates the ref resolves to a loaded SLI, so the
  resolver only sees consistent state.

The Generator interface gains a Validate() method that runs every
generator-side check (indicator resolution, label grammar, template
execution, alert structure) without touching the filesystem. The
existing createGeneratedFiles path is reused and its in-memory output
discarded.

Tests use goldie-backed golden files (test-singleline, test-multiline,
test-ratio, test-ratio-target-percent, test-tiered) so the rendered
Prometheus rules stay under snapshot review; tiered alert expectations
live alongside as a separate fixture.
opensloctl gets a third subcommand that runs every validation layer
(load-time + generator-side) without writing any output files:

- specstore.GetSpecs handles YAML parse, SDK validation, duplicate
  detection, ref resolution, and per-kind AlertCondition checks.
- prometheusgenerator.Validate runs indicator resolution, label
  grammar, template execution, and alert structure validation.

Exits 0 on success, 1 with a slog.Error on any failure — same
slog-as-error reporting style as the existing commands. The flag shape
mirrors load (-f repeatable, -r recursive), so users can swap one for
the other in CI hooks.
Two more complete example directories ship alongside api-latency-slo and
error-budget-slo, showcasing the remaining SRE alerting strategies:

- examples/error-rate-slo/   (kind: error-rate, absolute threshold)
- examples/multi-burn-slo/   (kind: multi-burn-rate, OR-ed windows)

Each new directory follows the same layout (service, datasource, sli,
slo, alert-condition-{page,ticket}, alert-policy-{page,ticket},
notification-target-...) as the existing examples, and ships with its
own README explaining the strategy and how to run/validate it.

The api-latency-slo and error-budget-slo alert conditions are upgraded
from the legacy "burnrate" kind to the kebab-cased "burn-rate" — the
generator still accepts "burnrate" for back-compat but new specs should
use the form documented in the README.

Per-directory READMEs are added for api-latency-slo and error-budget-slo
matching the new examples.
- README: add validate command, expand examples table to five
  directories, document label name grammar and multi-value validation,
  note that indicatorRef resolution is supported.
- AGENTS.md: extend commands section (three subcommands now), note
  scratch scripts must live in ./tmp (gitignored).
- .gitignore: track opensloctl binary in addition to existing patterns,
  add tmp/ scratch directory.
…on.target attributes

Adds two string attributes that the Prometheus generator stamps onto
every generated alert rule label. Both follow the Dot-name ↔
underscore-name split used by the existing registry entries
(openslo.slo.name → openslo_slo_name) so the Prometheus label keys
remain snake_case:

  openslo.alert.severity        → openslo_alert_severity
  openslo.notification.target   → openslo_notification_target

The generated constants in pkg/semconv/semconv_gen.go are refreshed
via make semconv-generate.
The Prometheus generator can render the routing target as a single
openslo_notification_target label, but only if each AlertPolicy
references at most one target. Pre-existing AlertPolicy → AlertNotificationTarget
ref resolution still runs; this new check additionally rejects any
AlertPolicy with len(spec.notificationTargets) > 1 so future generation
can never produce duplicate-label YAML.
buildAlertGroups now treats each AlertPolicy's single
notificationTargets[0].targetRef as the routing target for every condition
contributed by that policy. The resolved AlertNotificationTarget.spec.target
is aggregated per severity in the severityState; when multiple policies
for the same severity disagree on their target, slog.Error fires and the
openslo_notification_target label is omitted (rather than picking an
arbitrary value) so the misconfiguration is obvious in the rules file.

The AlertGroup struct gains a NotificationTarget string field that the
template renders conditionally; empty stays empty.
…_target labels

The Prometheus alert rules template now renders two new labels on
every alert rule, replacing the bare "severity" key and adding routing
info:

  openslo_alert_severity:        <severity>
  openslo_notification_target:   <target>      (only when present)

The label names match the weaver registry attributes (dots →
underscores for Prometheus compatibility).

Golden fixture for the tiered test scenario is regenerated to capture
the new labels with notification_target=engineers stitching through.
…and openslo.timewindow.duration

These three attributes were declared in the registry but never
referenced by any recording rule, golden fixture, or downstream
consumer. Mark them with the weaver `deprecated` block (obsoleted
reason) so downstream OTel tooling surfaces the deprecation while
keeping the constants available in pkg/semconv/semconv_gen.go for
backward compat with any pinned generator artifact.
Single integer gauge {0,1,2,3} = Healthy/Burning/Critical/Breached
derived from openslo_slo_current_burn_rate against overridable
thresholds. Defaults 1/6/14.4x per the SRE workbook. Overridable per
SLO via threshold.status.openslo.com/{warning,critical,breached}
annotations (work lands in the specstore validator + generator).

Emitted only for SLOs that reference at least one AlertPolicy.
DOCTYPE partition: integer gauge as a categorical scaffold for
dashboard rendering.
…positive

Implements the validator half of the SLO status gauge work.

Annotations on metadata.annotations:
  threshold.status.openslo.com/warning   (default 1)
  threshold.status.openslo.com/critical  (default 6)
  threshold.status.openslo.com/breached  (default 14.4)

Each annotation is optional; missing annotations fall back to defaults
independently. Non-numeric values silently fall back to defaults so
that scratch notes ("TODO") don't fail validation. The resolved triple
must be strictly ascending and positive; violations result in a
load-time error reported via ValidateRefs + surfaced through
`opensloctl validate`.

Table-driven tests cover partial overrides, defaults, ascending
violations, and zero/negative inputs.
Plugin half of the status-gauge work that pairs with the specstore
validator. The generator resolves the three status threshold
annotations (defaults from specstore exported constants) and feeds
them into the template, which renders an unsigned 0/1/2/3 recording
rule under a new openslo-status-recordings-<slo> group:

  0 (Healthy)     when current_burn_rate < warning (default 1x)
  1 (Burning)     when warning <= x < critical (default 6x)
  2 (Critical)    when critical <= x < breached (default 14.4x)
  3 (Breached)    when x >= breached

The structural shape uses disjoint branch comparisons so exactly
one value emits per (slo_name, spec_version). Multi-dim SLOs also
produce a openslo_slo_status post-process rule via label_join, like
the existing slo_info/slo_objective pattern.

Tests cover the generator's threshold-resolution helper via the
specstore ParseStatusThreshold + ValidateRefs paths; goldens are
regenerated under the existing test/ratio/multi-dim fixtures.
Ten new gauge metrics, one per multi-window (5m, 30m, 1h, 2h, 6h,
1d, 3d, 7d, 28d, 30d), mirroring the openslo.sli.error_rate_<window>
convention. Each carries events-per-second from the underlying SLI
source query — useful in Grafana dashboards for traffic-context
panels when interpreting error budget burn rate.

Emitted only for RatioMetric SLIs (where total.MetricSource.spec.query
is a counter rate); ThresholdMetric SLIs do not supply event count
queries so the recording rule is skipped for that path. Generation
logic comes in the next commit.
…RatioMetric

Template extension for the second half of the event-rate metric
work. The prometheus.go RatioMetric branch now carries the total
counter query alongside the error-rate query and renders it into
per-window event-rate rules under the existing sli-recordings
group:

  openslo_sli_event_rate_<window>{openslo_slo_name, ...} =
    sum(rate(<total source query>[<window>]))

ThresholdMetric SLIs skip event-rate emission entirely because the
heysers spec doesn't expose a parallel event-count query (avoids
the heuristic-parse failure path discussed in Fork E).

Multi-dim SLOs also get post-process label_join rules, matching the
existing pattern for the other windowed recordings on the same
schema. Ratio-only goldens (test-ratio-slo, test-ratio-target-percent,
test-multi-dim) are updated; threshold-only goldens (test-multiline,
test-singleline) confirm zero event-rate additions.
Extends the existing Recording Rules subsection with two pieces:

1. Inline bullet for openslo_sli_event_rate_<window>, scoped to
   RatioMetric SLIs only (ThresholdMetric skip explained).
2. New "Status gauge" subsection for openslo_slo_status with the
   0/1/2/3 mapping, SRE-workbook defaults, and the
   threshold.status.openslo.com/* annotation override knobs (including
   ascending-positive validation contract).
Single-page dashboard mirroring Grafana Cloud's Manage SLOs view.
Table panel of every SLO loaded by opensloctl, with:

- Service filter bar (label_values on openslo_service_name)
- Columns: SLO, Service, Objective, Window days, Current burn,
  Period remaining, Status
- Status rendered from openslo_slo_status with value mappings
  (0=Healthy / 1=Burning / 2=Critical / 3=Breached) and color cues
- Drill-down link from SLO cell to openslo-detail?var-slo=${slo}
- Sortable by Current burn × (descending by default)

Datasource wired as ${DS_PROMETHEUS} via __inputs so the JSON
imports cleanly into any Grafana deployment; provisioning binds to
the existing Prometheus datasource (webstore-metrics in the
oteldemo compose).
In-depth single-SLO dashboard mirroring the Grafana Cloud SLO
dashboard sections. Layout (24-col grid):

- Row 1 header stats: SLO name, Service, Objective gauge, Window
- Row 2: SLI rate timeseries (with objective as a dashed reference
  line) + SLI 28d average stat
- Row 3: period error budget remaining timeseries + Status stat
  (0/1/2/3 mappings: Healthy/Burning/Critical/Breached)
- Row 4: current burn rate timeseries with horizontal threshold
  markers at 1x, 6x, 14.4x (SRE workbook burn-rate reference points)
- Row 5: event rate 28d timeseries (RatioMetric SLIs only; falls
  back to flat 0 for SLOs without an event_rate metric) +
  Active SLO alerts table

Multi-dim SLOs are picked up automatically via a $dim custom
variable that captures the dimension prefix from the SLO name
(when present).

Datasource wired as ${DS_PROMETHEUS} via the same __inputs pattern
as the list dashboard; provisioning binds in deployment.
…shboards

Standard Grafana provisioning block scoped to a single "openslo"
provider and an OpenSLO folder. Single path option points at the
container-internal directory that compose.slos.yaml mounts; the
host path is the repo-root deploy/dashboards/.

Type: file, updateIntervalSeconds: 30, allowUiUpdates: true so the
JSON dashboards stay editable in the UI but auto-refresh when the
repo file is changed during dev iterations. disableDeletion: false
keeps the dashboards re-creatable when files are removed.
Adds a single host-to-container bind mount: the repo-root
`deploy/dashboards` directory is exposed at
`/etc/grafana/provisioning/dashboards` inside the Grafana container,
where the file-provider (examples/oteldemo/openslo-dashboards.yaml)
scans for dashboard JSON files.

The dashboards/*.json files + provisioning yaml are picked up on
the next Grafana start; in-flight edits to JSON files are picked up
within updateIntervalSeconds (30s) without a restart.
The templated openslo_slo_status rule emitted double "}}" — a literal
typo in the source. The Go template parser preserved it as text,
producing promql like
  openslo_slo_current_burn_rate{...="v1"}} >= 14.4
which promtool rejects with "unexpected character: '}'".

Trim one extra closing brace so the rendered label-selector closes
with a single '}'. Golden files regenerated; oteldemo rules pass
promtool check with all 12 files reporting 30 rules SUCCESS.
Aligns the README's Attribute/Metrics table with the live weaver
registry after this round of work. Additions:

- Attributes: openslo.service.name, openslo.alert.severity,
  openslo.notification.target.
- Mark the three deprecated attributes (decimal/percent/duration)
  in their own table with their obsoleted reason — users can read
  why they're deprecated without grepping the registry.
- Add the four SLO info metrics that didn't exist when the section
  was first written: current_burn_rate, period_burn_rate,
  period_error_budget_remaining, status (with cross-link to the
  Status gauge subsection in Recording Rules).
- Add the SLI event rate metric family with the explicit
  RatioMetric-only constraint.

Consuming-the-Registry example now shows the status gauge constant
in addition to openslo_slo_info.
The original mount only carried the JSON dashboard files. The
file-provider MUST live in the same container directory the JSONs
live in or the provider isn't loaded at startup — which is why no
"OpenSLO" folder appears in the Grafana UI.

Add a second bind-mount for the single provisioning yaml file with
the same container directory as its target, so Grafana picks up
both the provider registration and the dashboards it points at.
Local opencode CLI config holding per-developer MCP server command/env.
Project files only live in the worktree, not in shared history.
Job steps include goreleaser but the workflow also runs go build,
go test, and lint; the previous goreleaser name was misleading.
PromQL parses left-to-right at equal operator precedence so the previous
"1 - (\nXs\n)\n/\n(\nYs\n)" template emitted (1 - good) / total — the
success rate, not the error rate. Downstream burn-rate math inverted: a
healthy service read as burning thousands of times its budget, which
poisoned the categorical status gauge and turned dashboards red.

Move the slash inside the outer parentheses so the expression evaluates as
1 - (good / total). Two ratio golden files regenerated; the rest of the
testdata stays untouched so future commits can attribute their own diffs.
The categorical status block emits three integer statuses (0/1/2/3) by
multiplying the result of a comparison against burn-rate thresholds:

  (burn_rate >= 14.4) * 3            # Breached
  (burn_rate >= 6 and burn_rate < 14.4) * 2
  (burn_rate >= 1 and burn_rate < 6) * 1

Comparison operators between an instant vector and a scalar are filters
by default in Prom >= 0.19.0 - the surviving series pass through with
their LHS values. Without the bool modifier, '(burn_rate >= 14.4) * 3'
multiplies the raw burn rate (e.g. 57.3) instead of returning 3, so the
status gauge emits the wrong integer and the dashboard's
0/1/2/3 -> Healthy/Burning/Critical/Breached mapping never matches.

Add 'bool' to every comparison in the status block template. Alert-rule
expressions continue to use filter semantics on purpose - when an alert
evaluates to 'the burn-rate series survived the comparison', each
surviving series is itself a violating series, so 0/1 coercion would
hide violations.

Add TestStatusRuleUsesBoolModifier:
- 3 '>= bool' (Warning, Critical, Breached) and 2 '<  bool' (Breached,
  Critical) checks the status block of every generated rule.
- Top tier multiply-by-3, mid multiplies by-2, lowest by-1 - the integer
  status codes the dashboard maps to text.
- Sanity guard: no bool survives into the alert block.

Tested across multiline, multi-dim, and tiered fixtures so a future
template refactor cannot silently regress.

Regenerated all matching fixtures via TestGenerate_Golden -update. The
non-ratio testdata also picks up parenthesisation from the preceding
fix(generator) commit because they were not regen'd at that step; this
is harmless because both fixes land before any consumer runs 'make
generate' against the spec bundle.

The bool modifier has been stable since Prom 0.19.0 (Oct 2015), so the
output rules work on every Prometheus version in practical use.
Alert rules for one SLO previously shared an alert name across severities
(`AdAvailabilityMultiWindowMultiBurnRate` regardless of severity) and
relied on the `severity` label to differentiate. Inside a single rule
group that works, but cross-group deduplication per Prom's name uniqueness
requirement gave little headroom - any directory that consumes the same
generator output for two different sources (rules CM fixtures, hub-spoke
Prom federations, multi-tenant Prom with the same UID prefix) collapses
two semantically distinct alerts.

Append the severity PascalCase to the alert name so two alerts of the
same SLO and kind can coexist with distinct names:

  AdAvailabilityMultiWindowMultiBurnRatePage
  AdAvailabilityMultiWindowMultiBurnRateTicket

The `severity` label is unchanged so existing Grafana Alerting / Prometheus
alertmanager routing still work; only the alert name string is altered,
which is a human-readability and Prometheus uniqueness win, not a routing
change.

Breaking but not flagged in CHANGELOG [Unreleased] yet because prior
consumers referenced alert names for KPI dashboards or runbook titles.
This commit message registers it; a separate CHANGELOG entry will follow
under v0.2.0.

Only test-tiered-slo-rules.golden.yaml carries alert-name material
(other fixtures' specs do not reference AlertPolicies), so a single
golden update is enough.
The OpenSlo spec carries a free-form spec.description per SLO. Until now
opensloctl threw that away - the dashboard could show the SLO name,
target and burn rate but never the human prose, so alert runbooks and
on-call context flowed either through PromQueries manually or through a
stale notebook.

Add an openslo_slo_description label to the openslo_slo_info
recording rule, populated by a small foldSloDescription pipeline in
the generator:

  1. Replace every newline with one space.
  2. Collapse runs of whitespace to one space (strings.Fields + Join).
  3. Trim leading/trailing whitespace.
  4. Cap at 200 chars; truncate with unicode ellipsis U+2026 if longer.
  5. Escape quote and backslash so the value is safe inside
     YAML/PromQL quoting.

Empty input still produces empty string. Every SLO carries the label,
so downstream Grafana text panel macros of the form
${openslo_slo_description} always substitute to a stable row even when
the spec author skipped description.

Add the matching openslo.slo.description semconv attribute (stability
development) and a recommended reference on the metric emission
side. Regenerate pkg/semconv/semconv_gen.go so downstream code that
imports the constants compiles against the new attribute.

Regenerated testdata via TestGenerate_Golden -update. All six ratio
SLI and threshold SLI goldens pick up the new label line; alert-name
suffix and status bool changes from preceding commits are already
landed in these goldens so the diff is a single line per file.

BREAKING (registration only): series for openslo_slo_info gain a new
label. Dashboards matching on exact label sets will see the new
series in addition to the old. Recording-rule label cardinality of
the metadata series grows by one per SLO.
The decoder loop in loadSpecs dropped every YAML file that failed
decode with no log and no error, so a stray top-level field on an
OpenSlo spec, an outdated SDK field, or a non-OpenSlo YAML file
silently vanished from the generator run. Consumers got a rules set
that's strictly smaller than their spec bundle makes.

Two changes:

1. loadSpecs returns the loaded objects, the skipped filenames, and a
   non-nil error if any file failed. Each skipped file logs a
   structured slog.Warn with the filename and the decoder error so an
   operator can grep the run output to find the offending spec.

2. GetSpecs propagates that error wrapped as "error reading specs" so
   `make generate` and `make verify` exit non-zero. CI pipelines that
   have been quietly tolerating stray YAML now surface the issue at
   the same place the rest of specstore validation lives.

Move the test fixtures that intentionally fail to decode from
testdata/{invalid,non-openslo}.yaml into testdata/invalid/* so the
recursive walker doesn't pick them up by accident. The two cases that
walk testdata recursively now pass an explicit whitelist of known-good
files. Add an explicit table row "recursive loading surfaces invalid
fixtures as error" that includes one known-good file and the invalid
folder - the error wins, the loader doesn't quietly drop them.

BREAKING (CI integration only): previously-silent stray YAML files
that did not decode as OpenSlo now make opensloctl exit non-zero. CI
pipelines that relied on the silent drop will start failing - this is
the intended outcome, but flag it in release notes so integrators can
clean up stray fixtures deliberately.
The repo README and AGENTS.md style guide prefers ASCII hyphens in
prose. Earlier mass-edit sweeps caught most files; the few leftover
em dashes in the generator comments (function doc blocks in
prometheus.go and one comment block in templates.go) were bundled
into feature commits and would have polluted those diffs. Pulling
them into a single style commit here keeps the prior generator
features single-purpose and easier to audit.
Replace the previous broad-12 SLO inventory with an 11 SLO bundle focused
on user-perceivable signals. Each SLO pairs with one rule file under
examples/oteldemo/rules/, regenerated with the multi-window-multi-burn-rate
alert strategy across two paired tiers per severity (page/ticket x fast/slow).

Inventory changes vs the prior 12:

  ad-availability.yaml                - kept, narrow objectives to target 0.99
  ad-latency.yaml                     - new, ratio SLI on classic bucket
                                       le="2000" (frontend SERVER GET /api/data)
  cart-availability.yaml              - kept
  frontend-availability.yaml          - kept, simplified
  image-loading-latency.yaml          - new, source service_name="image-provider"
  order-processing-latency.yaml       - new, ratio SLI on le="60000"
                                       post-checkout fan-out burst
  payment-unreachable.yaml            - renamed + narrowed from the older
                                       payment-availability shape; trips
                                       when paymentUnreachable flag throws
  post-order-email-availability.yaml  - new, internal HTTP POST
                                       send_order_confirmation trips on
                                       emailMemoryLeak
  post-order-email-latency.yaml       - new, le="30000" envelope for the same
  product-catalog-availability.yaml   - kept
  recommendation-availability.yaml    - kept
  (alert-conditions.yaml, alert-policies.yaml,
   notification-target-engineers.yaml,
   services.yaml)                     - helper files shared across the 11 SLOs

The 8 reusable AlertConditions (page-fast-5m/1h at 14.4x, page-slow-30m/6h
at 6x; ticket-fast-2h/1d at 3x, ticket-slow-6h/3d at 1x) apply to all 11
SLOs without per-SLO duplication.

Each rule file is a recording rule group + alert group for that SLO
plus its referenced policies. Verified by cd /Users/ibrahimd/repos/github.com/thisisibrahimd/opensloctl.git/.worktrees/demo-slos && go run . load -f examples/oteldemo/specs/ -r (promtool check rules: 30 rules per file, SUCCESS across
all 11).

Note on rule files in this commit: they are generated output, not
sources, but they are checked into the repo so first-time users can
clone and immediately see a working ruleset without running
go run . generate -f  -o  first. The examples/oteldemo/.gitignore deliberately
excludes costs of re-generation but keeps a clean baseline here.
Replace the docker-compose based deployment harness with a kind cluster
running the upstream OpenTelemetry Demo via Helm. The compose setup
struggled with bind mounts when same target dirs (Grafana provisioning
dashboards, Prometheus rule_files) needed both upstream and custom
content; Kubernetes ConfigMaps sidestep both layering conflicts.

Layout under examples/oteldemo/kind/:

  - kind-config.yaml: minimal cluster spec - 1 control-plane node,
    port-forwards enabled so kubectl handles the routing surface.
  - setup.sh: creates cluster if missing, runs helm install/upgrade with
    the values overlay, applies our ConfigMaps (rules + dashboards),
    and port-forwards Grafana/Prometheus/Frontend on 8080/3000/9090.
  - sync.sh: replaces the openslo-rules and openslo-dashboards ConfigMaps
    (delete + create) after every make generate, then HUPs Prometheus -
    avoids the 256 KiB kubectl.kubernetes.io/last-applied-configuration
    annotation ceiling that the apply-only flow hit when the ruleset grew.
  - teardown.sh: deletes the kind cluster and the deployed Helm release.

Overlay values.yaml customises the chart for our SLO telemetry:

  - otel-collector: single span_metrics connector on namespace
    traces.span.metrics (sub-named connector ids were rejected by the
    chart), extended explicit histogram bucket list to include 30s and 60s
    so the ad-latency (le=2000), post-order-email-latency (le=30000) and
    order-processing-latency (le=60000) SLOs find valid boundaries.
  - Grafana: anonymous admin enabled, sidecar dashboards watching the
    gfd-sc-dashboards ConfigMap.
  - Prometheus: sidecar configmap-reload on the prometheus ConfigMap so
    rule_files picks up new entries without a Prometheus restart.

examples/oteldemo/Makefile:

  - verify - open the spec bundle without writing rules.
  - generate - regenerate rules + promtool lint.
  - lint-rules - promtool check rules on every rules file.
  - start-demo / stop-demo / clean / sync - thin wrappers around the
    kind scripts.

Removes the prior docker-compose harness:

  - examples/oteldemo/deploy/compose.slos.yaml (docker-compose snippet,
    no consumer)
  - examples/oteldemo/openslo-dashboards.yaml (legacy direct-mount
    dashboard format that the kind sidecar replaces)

The root Makefile's start-demo target previously drove the compose
harness; the kind setup.sh now owns the lifecycle and the per-example
Makefile is the single entry-point for new users.
The repo-root dashboards and integrity rules now have a real source of
truth under deploy/mixins, grafonnet v13 jsonnet. The build pipeline:

  deploy/mixins/*.jsonnet   -- jsonnet + sprig --> deploy/mixins/dashboards/*.json
                          \                          |
                           -- sync-legacy (Makefile)--> deploy/dashboards/*.json
                                                       |
                                                       -- (kind/sync.sh) --> openslo-dashboards CM

deploy/mixins/dashboards

  - openslo-list.json rendered from openslo-list.jsonnet: single table
    panel with one row per SLO. Columns: Objective %, Period SLI, Status
    (0..3), Budget Left %. Status mappings are array-form with a value
    per entry so Grafana treats them as value mappings; cellOptions
    applies a tinted-background colour via thresholds, not via mappings.
    Budget bands: red<15, yellow 15..49, green >=50.
  - openslo-detail.json rendered from openslo-detail.jsonnet: a text
    header (name/description/target) plus three timeseries columns
    (SLI 28d, Error Budget Burndown, Burn Rate) and three stat columns
    (SLO target, 28d SLI, 28d Remaining %, Current Burn Rate).
    Backing series: openslo_slo_info, openslo_slo_objective, status, and
    the windowed sli_error_rate_* recording rules emitted by the
    generator.

Variables: datasource (Prometheus picker, pluginId locked lowercase),
slo (label_values over openslo_slo_info). Two hidden helpers -
description and target - feed the markdown header text panel without
showing up in the picker.

deploy/mixins/rules/openslo-integrity.jsonnet --> deploy/rules/
openslo-integrity-rules.yaml via jsonnet -S

  - openslo_slo_metric_missing: count by openslo_slo_name of (info
    exists but sli_error_rate_5m has no sample for the last 10m).
    Caught drift scenarios like a typo in spec indicator, a refused
    target Prometheus access, or a generator run that didn't include a
    particular SLO.
  - OpenSloSpecDrift (page severity, 10m confirm window) surfaces
    culprit SLOs into Alerting > Alert rules > Data source-managed so
    an operator catches spec drift before a customer does.

deploy/mixins/Makefile

  - generate / list / detail / rules / lint-rules / release:
    jsonnet -> dashboard JSON, jsonnet -S -> rules YAML, promtool
    check rules on every output. version extVar pins a git-SHA + dirty
    marker in the rendered dashboard title.

deploy/dashboards/* is checked in but should be treated as generated
output - any edit must be in deploy/mixins/* first, then
re-applies. The legacy sync to deploy/dashboards/ is weaver registry generate --v2 --registry semconv/registry --templates ./semconv/templates/ go ./pkg/semconv/
gofmt -w ./pkg/semconv/semconv_gen.go (target name from the existing Makefile).
Carry the v0.2.0 contract end-to-end so users hitting the README
without prior context can ship a working dashboard within an hour:

  - Root README: rewrite around nine sections with a TOC (Quick start,
    What gets generated, Writing specs, Alerting strategies, Multi-dim
    SLIs, Dashboards and integrity rules, Examples, Semantic
    conventions, Development). The spec tour lands before any
    alerting deep-dive, following the path a first-time reader takes.

  - Names and references for every metric and every attachment point:
    openslo_slo_info/.object/.timewindow/.error_budget/.current_burn_rate,
    openslo_sli_error_rate_<window>, openslo_sli_event_rate_<window>,
    openslo_slo_period_burn_rate, openslo_slo_period_error_budget_remaining,
    openslo_slo_status, openslo_slo_description. Plus the dashboards
    that consume them and the integrity rules that catch spec drift.

  - Alert naming convention updated to reflect the severity-suffixed
    alert name introduced in this version: AdAvailability...Page and
    AdAvailability...Ticket rather than the shared name. The severity
    label is unchanged so existing Promql alertname/severity JavaScript
    template routes still work; only the alert name string grew.

  - Examples table includes multi-dim-slo with its dedicated README
    and properly attributes the harness (kind + Helm) to examples/oteldemo.

  - Semantic Conventions section notes the three deprecated attributes
    with planned major-removal paths.

  - Development section mentions mise pinning Go 1.26 with go.mod
    min 1.25.5, and the make snapshot-update path via go test -update.

Per-example READMEs (api-latency, error-budget, error-rate, multi-burn,
multi-dim, oteldemo) keep the previous example-by-example shape but
drop stale URLs (no more C2%A7 escape sequences from pre-existing
markdown; replaced with hyphened section slugs) and route readers back
to the root README for shared concepts.
Fold the prior [Unreleased] and [v0.1.8-dev] sections into a single
[v0.2.0] release. The branch is 28 commits and 13 working-tree commits
ahead of v0.1.0; everything user-visible since v0.1.0 lives in this
release.

Top-level fingerprint:

  ### Breaking changes (4)
    - loadSpecs fail-loudly on decode errors
    - openslo_slo_description label added to openslo_slo_info sidecars
    - alert names now suffixed with severity (Page / Ticket)
    - oteldemo docker-compose harness replaced by kind + Helm

  ### Added (10)
    Four-kind alert generation, multi-window-multi-burn-rate reusable
    condition set, status gauge, multi-dim SLI annotations, grafana
    dashboards with mixins source of truth, spec-drift integrity
    rule + alert, oteldemo kind+Helm harness, semconv
    openslo.slo.description attribute, TestStatusRuleUsesBoolModifier
    regression test, multi-dim-slo example.

  ### Changed (6)
    openslo_slo_description label emission, alert naming convention,
    oteldemo spec inventory 12 -> 11, Path Y migration for 6 SLOs,
    chaos_flag drift documented in AGENTS, per-SLO target locking.

  ### Fixed (3)
    Ratio precedence bug, status gauge bool modifier for Prom 3.x,
    specstore fail-loud loadSpecs.

  ### Removed (3)
    oteldemo/deploy/ compose harness, oteldemo/openslo-dashboards.yaml,
    top-level Makefile start-demo target.

  ### Deprecated (1)
    Three semconv attributes (decimal, percent, timewindow.duration)
    slated for major-release removal.

No [Unreleased] section per request - the next commit sets the next
version line directly.

[Unreleased]: v0.2.0...HEAD
[v0.2.0]:     v0.1.0...v0.2.0
Hunt aggregate missed two more files when compared to the main
lineage: feature.go's MULTI_DIMENSIONAL annotation constants and
labels.go's ParseSpecLabels comment. Replace em dashes with ASCII
hyphens to keep the prose consistent with the rest of the repo.
Round out examples/multi-dim-slo/ with the spec bundle backing the
README. Each file plays a role described in the file layout table:

  service.yaml - api-gateway Service definition
  sli.yaml     - api-gateway-latency-sli with thresholdMetric over
                 http_request_duration_seconds_bucket
  slo.yaml     - api-latency SLO with multi-dimensional-sli.openslo.com
                 /label=service_name and /dimensions annotations; pulls
                 SLO target via indicatorRef
  alert-condition-page.yaml - burn-rate 14.4x over 5m
  alert-condition-ticket.yaml - burn-rate 3x over 2h
  alert-policy-{page,ticket}.yaml - AlertPolicy wrappers with one
                 condition each (SDK enforces exactly one)
  notification-target-{pagerduty,slack}.yaml - NotificationTarget
                 registry so the AlertPolicy can route fires

Add deploy/mixins/alerts/.keep as a placeholder directory; future
grafonnet alert definitions will live here so the dashboards folder
has a peer for alert-manager provisioning. The empty directory needs
.gitkeep style scaffolding to survive an empty-commit cleanup; the
.local .keep file carries a single comment rather than the typical
.gitkeep name to flag it's a scaffold.
Audit findings (GitHub Actions hardening):

  HIGH - goreleaser/goreleaser-action@v5 was a mutable tag. Compromised
  upstream rewrites v5; OIDC + a token-bearing runner. Pin to v7.2.3
  commit SHA f06c13b6 with version comment.

  LOW - actions/checkout@v4 was mutable. Pin to v7.0.1 commit SHA
  3d3c42e5.

  LOW - actions/setup-go@v5 was mutable. Pin to v7.0.0 commit SHA
  b7ad1dad.

  LOW - go-version was 'stable' (unpredictable). Pin to '1.26.x' so
  CI matches the release toolchain in mise.toml.

  MEDIUM - permissions was a single top-level block granting
  contents: write to every job. Split per job to deny-by-default:

    snapshot (PR): id-token: write + contents: read.
    release (tag): id-token: write + contents: write.

  The snapshot job on pull_request never needs to write tags and
  cannot reach secrets in fork PRs, so contents: read is the right
  scope. contents: write stays on the release job only.

  Default-deny at workflow level: a top-level `permissions: {}` is
  the safe root of trust - every job walks back up to that root
  before declaring its own needs.

  Rename inner jobs: goreleaser -> snapshot / release so the workflow
  file mirrors the goreleaser lifecycle phases rather than wrapping
  one outer 'goreleaser' job that branches by event_name.

  Add cache: true to setup-go so Go module downloads warm across runs.

  Add fetch-tags: true to the release job's checkout so goreleaser
  picks up the just-pushed vX.Y.Z tag without an extra git fetch.

Trigger review: pull_request (same repo / no secrets) and
release/published (privileged, gated by repo maintainers who can
publish a release). Neither path has script injection sinks - every
run: block uses static args. The GITHUB_TOKEN used by the goreleaser
action is the per-run runner token, not a long-lived PAT.

All three SHA pins annotated with their upstream tag in a trailing
comment so Dependabot can scan and update them in batch via
github-actions ecosystem.

SHA source: GitHub REST `GET /repos/{owner}/{repo}/tags` at
this commit, picking the first matching semver. Each pinned SHA is
the commit object that the tag points at, not an arbitrary branch
tip, so the supply chain locks rather than floats.
@thisisibrahimd thisisibrahimd added documentation Improvements or additions to documentation enhancement New feature or request labels Sep 8, 2026
@thisisibrahimd thisisibrahimd self-assigned this Sep 8, 2026
@thisisibrahimd
thisisibrahimd merged commit 99157ef into main Sep 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant