Skip to content

feat: rule playground (ding test-rule + ding run --dry-run)#14

Merged
zuchka merged 13 commits into
mainfrom
add-rule-playground
May 8, 2026
Merged

feat: rule playground (ding test-rule + ding run --dry-run)#14
zuchka merged 13 commits into
mainfrom
add-rule-playground

Conversation

@zuchka
Copy link
Copy Markdown
Collaborator

@zuchka zuchka commented May 8, 2026

Summary

Two new preview surfaces so users can verify ding.yaml rules without running a real workload.

  • ding test-rule — replay JSONL events through the rule engine; matching rules render their messages but no notifier sends happen. Stdin or file input; per-event, windowed (avg(value) over 5m > X), and mode: end-of-run rules all supported. Synthetic 1s-apart timestamps when events omit a timestamp field.
  • ding run --dry-run — wraps a real command but suppresses notifier sends; runctx detection, end-of-run rules, synthetic run.exit, and exit-code propagation are all unchanged. Preview output goes to stderr so the wrapped command's stdout stays clean.
  • Internal: small Dispatcher interface refactor (internal/cli/dispatcher.go) lets both surfaces swap NotifierDispatcher for a LoggingDispatcher (internal/dryrun/). Format auto-detects: text on TTY, JSON when piped; --format text|json and --no-color overrides on both surfaces.

This branch also bundles the prior CircleCI-removal commit (549fe6e) — see the rationale in that commit's message.

Item 1 of 4 in an internal inward-polish roadmap (test-bench → run-lifetime windowing → recipe sweep → template helpers).

Test plan

  • go test ./... green (all 13 packages)
  • go test -race ./internal/cli/... ./internal/dryrun/... clean
  • Manual smoke: ding test-rule events.jsonl (text on TTY, JSON when piped); ding run --dry-run -- failing-script.sh (exit code propagates, alerts to stderr, zero notifier sends)
  • CI green
  • Reviewer to verify: dry-run never sends — internal/cli/run_test.go::TestIngestStream_DryRun_NoNotifierSends is the canonical guarantee

🤖 Generated with Claude Code

zuchka and others added 13 commits May 8, 2026 13:24
Pulls the per-alert dispatch logic out of run.go's free functions and into
a NotifierDispatcher behind a small Dispatcher interface. Behavior is
identical; existing tests pass unchanged after a one-line construction
update.

Sets up upcoming `ding test-rule` and `ding run --dry-run` to swap the
real dispatcher for a logging one without further engine or ingest
changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders evaluator.Alert as human-readable multi-line text with optional
ANSI color, used by upcoming `ding test-rule` and `ding run --dry-run`
TTY output paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from code review on c864be4:

- Apply ansiDim to the "message:" label in the color path so the third
  line is consistent with the dim treatment on the metric/value line.
  Message body stays unstyled (it's user-rendered template content,
  not formatter chrome).
- Color test now asserts ansiReset is present in the output, catching
  any future change that opens color codes without closing them
  (which would leave the terminal styled until the next prompt).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JSONL output (one JSON object per line, newline-terminated). Stable
schema with aggregate fields always present (zero when non-windowed) so
jq pipelines don't have to nil-check. Used for --format json and the
piped-stdout default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three follow-ups from code review on de751ab:

- TestJSONFormatter_RoundTrips wantKeys now includes max/min/count/sum
  alongside avg, locking the "aggregates always present" invariant in
  the test (not just the comment).
- TestJSONFormatter_NilNotifiers verifies the nil-coercion path: when
  Alert.Notifiers is nil, the JSON output renders alerts as [] not
  null. The round-trip test only exercised the populated case.
- Code comment above json.Marshal call documents why the discarded
  error is safe (envelope contains only stdlib-marshalable types).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sends

Satisfies cli.Dispatcher implicitly via Go duck-typing — the dryrun
package does not import the cli package. Both upcoming surfaces
(test-rule, run --dry-run) construct one of these instead of a
NotifierDispatcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New subcommand that loads a ding.yaml, reads JSONL events from stdin or
a positional file, and routes engine alerts through a LoggingDispatcher
instead of real notifiers. Synthetic timestamps for events that omit
the timestamp field; end-of-run rules fire after the last event.

Format auto-detects: text on TTY, JSON when stdout is piped. --format
text|json overrides; --no-color disables ANSI.

Extends timestamp parsing to accept RFC3339 strings (via resolveTimestamp)
in addition to the Unix epoch floats that ParseJSONLine already handles.
Fixture YAML uses correct map-keyed notifiers and alert target format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three follow-ups from code review on 3b80c19:

- resolveTimestamp now returns (time.Time, bool); the loop in
  runTestRule branches on the boolean rather than on ev.At.IsZero(),
  which was always false because ingester.ParseJSONLine populates At
  with time.Now() unconditionally. Effect: events without a timestamp
  field now actually get the synthesized 1s-apart timestamps the docs
  promise, fixing windowed-rule authoring without explicit timestamps.
- runTestRule rejects --format values other than auto/text/json with a
  descriptive error, instead of silently falling through to auto.
- Three new tests: synthesized-timestamp path; invalid --format
  rejected; malformed JSON line is logged + skipped + subsequent valid
  line still fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds --dry-run, --format, and --no-color flags to `ding run`. When
--dry-run is set, notifier construction still happens (for config
validation) but the dispatch path is swapped for a LoggingDispatcher
that writes formatted alerts to stderr. All other run-flow behavior
(subprocess wrap, runctx, end-of-run rules, exit code propagation) is
unchanged.

Stdout stays reserved for the wrapped command's mirrored output;
preview alerts and operational logs both go to stderr to avoid
contaminating the wrapped command's stdout pipeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from code review on aee0184:

- Move dry-run --format validation to the very top of runRun so a
  --format typo doesn't pay the BuildFromConfig + notifier-worker
  startup cost before being rejected. Practically minor (CLI exits
  immediately) but the ordering is correct as-is.
- drainOnce now skips drainNotifiers + alertLogger.Close in dry-run
  mode. No alerts were ever queued (LoggingDispatcher doesn't call
  Send), so the drain pays the full drain_timeout to flush empty
  queues for nothing.
- --format flag description now says "when --dry-run is set" so users
  don't expect it to affect non-dry-run output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New "Testing rules without a workload" section in configuration.md
covering both surfaces. README quickstart pointer; ding.yaml.example
comment block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups from review on d2584e4:

- timestamp field description now mentions both RFC3339 strings and
  Unix epoch numbers (test-rule's resolveTimestamp accepts both;
  earlier text omitted Unix epoch).
- "JSONL when piped" → "JSON (one object per line) when piped" to
  avoid confusion with --format json (which is the actual flag value).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three follow-ups from final cross-branch review on 53b39b0:

- docs/configuration.md: the `--dry-run --format json | jq` example
  was broken because dry-run preview goes to stderr, not stdout. Added
  `2>&1` so the redirect actually pipes preview into jq.
- docs/configuration.md: corrected the "uncontaminated" claim. Child
  stderr IS mixed with preview lines (ingestStream mirrors stderr +
  LoggingDispatcher writes to stderr); only stdout stays clean.
- internal/cli/test_rule.go: format validation now runs before
  BuildFromConfig, matching the pattern fixed for runRun in 711e846.
  Same code-smell rationale: don't pay the config-load cost just to
  reject a typo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zuchka zuchka merged commit 9f2781f into main May 8, 2026
1 check passed
@github-actions github-actions Bot locked and limited conversation to collaborators May 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant