Namespace Kuestenlogik.Bowire.Monitoring

Classes

BowireMonitoringEndpointContribution

Discoverable endpoint-mount entry point for the Monitoring workbench surface (#102). Picked up by Core's BowireApiEndpoints scan via the IBowireEndpointContribution seam — splices the read-only /api/monitoring/* endpoints into the auth-gated bowireGroup.

BowireMonitoringRailContribution

Monitoring rail contribution (#102) — the read-only workbench surface over the probe outcome ledger: live status per probe, sparkline strip, historical outcome table. Probes are authored as files and run by bowire monitor run; the rail never mutates the ledger.

BowireMonitoringServiceContribution

Discoverable service-registration entry point for the Monitoring workbench surface (#102). Picked up by Core's AddBowire assembly scan via the IBowireServiceContribution seam. Registers only the outcome ledger at the default root — the read path the workbench rail needs. The probe engine (runner / scheduler) stays opt-in via AddBowireMonitoring(IServiceCollection, string?), which also wins the ledger registration when a host calls it with a custom root before the scan runs.

ConsoleSignaler

The always-on Core signaler — writes a prominent line to a TextWriter when a probe crosses the pass↔fail line. This is the default channel the CLI registers; the network signalers (Slack / PagerDuty / OTLP) are opt-in sibling packages layered on top.

MonitoringEndpoints

Workbench-facing read endpoints for the Monitoring rail (#102). The rail is strictly read-only: probes are authored as files and run by bowire monitor run (or an embedded scheduler); the workbench only renders the outcome ledger — live status, per-probe sparkline, and the historical outcome table. Both endpoints read the ledger on request, so the surface stays live against a monitor process appending from another process — no cache, no push channel needed at the probe cadences involved.

MonitoringServiceCollectionExtensions

DI wiring for Monitoring (#102). Registers the Core engine — the outcome ledger, the probe runner, and the TimeProvider-backed scheduler. Registers no ISignaler: outbound channels are opt-in and contributed by their own packages, so a host that calls only this runs probes and writes the ledger without any outbound call. The host must supply an IProbeExecutor (the recording-replay implementation).

MonitoringTelemetry

The monitoring metrics surface (#102) — a single Meter with the two instruments the design calls for, in the shared bowire.* namespace so #29's Grafana dashboards gain a section rather than a parallel one:

  • bowire.monitoring.probe.duration — histogram of run latency (ms).
  • bowire.monitoring.probe.outcome — counter, tags: probe + outcome.

Recording is dependency-free (System.Diagnostics.Metrics is in the BCL) and cheap when no listener is attached, so the engine records on every run. The OTLP export lives in the opt-in Kuestenlogik.Bowire.Monitoring.Otlp package; without it the instruments still record in-process (readable by the workbench surface).

OutcomeLedger

The append-only outcome ledger (#102, Decision 2). Each probe gets a <name>.jsonl file under the ledger root; every run appends one JSON line. The last row is the source of truth for restart resume (when did this probe last run) and transition detection (was it passing or failing). Reading the last row is a cheap tail read; the workbench surface reads the whole file for the sparkline.

Probe

A Monitoring probe (#102) — a saved invocation (BowireRecording) plus the three extras that turn it into a scheduled health check: a Schedule, the Assertions that must pass, and a Severity that routes the signal. This is the sentence the whole engine serves: "a probe is a recording with a schedule, assertions, and a severity."

ProbeAssertion

A must-pass predicate over a probe's response. v1 covers the three checks a black-box health probe actually needs: an expected status, a latency budget, and a body substring. Richer predicates (JSON-path, header, regex) converge on the shared Flows expectation DSL in a follow-up; this minimal set keeps the Core engine self-contained.

ProbeAssertionVerdict

One assertion's verdict — did it pass, and a human-readable why.

ProbeExecutionException

Thrown by an IProbeExecutor when a probe run couldn't complete. The runner catches this (and only this) to record an Error outcome.

ProbeExecutionResult

The raw result of running a probe's recording once, before assertions are applied. Produced by an IProbeExecutor.

ProbeFile

Loads a probe definition file — a saved recording wrapped with the three Monitoring extras (schedule, assertions, severity):

{
  "name": "payments-health",
  "schedule": "every 60s, 09:00-17:00 UTC, Mon-Fri",
  "severity": "crit",
  "assertions": [
    { "kind": "status", "expected": "200" },
    { "kind": "latencyBelowMs", "expected": "500" },
    { "kind": "bodyContains", "expected": "healthy" }
  ],
  "recording": { "id": "...", "name": "...", "steps": [ ... ] }
}

A malformed file throws ProbeFileException with a clear reason (the CLI reports it and skips that probe — visible, non-silent).

ProbeFileException

Thrown when a probe file can't be read or parsed.

ProbeOutcome

One row in a probe's outcome ledger — the durable record of a single run. Serialised one-per-line into <probe>.jsonl; the last row is the source of truth for restart resume + transition detection (Decision 2).

ProbeRunner

Runs a probe once and records the outcome (#102). The pipeline per run: execute the recording → evaluate the assertions → append the outcome to the ledger → detect a pass↔fail transition against the ledger's previous row → fire every configured signaler on a transition. This is the single unit the scheduler invokes on each tick.

ProbeSchedule

When a probe runs (#102, Decision 1). v1 accepts an interval (every 60s, every 5m, every 2h) optionally bounded by a UTC time-of-day window and a weekday set (every 5m, 09:00-17:00 UTC, Mon-Fri). Anything the parser can't model returns a clear error rather than silently approximating — the operator sees "unsupported schedule" and the probe simply isn't scheduled. Full cron expressions are a follow-up.

ProbeWindow

A UTC time-of-day window plus a weekday set. A probe only fires inside the window; outside it, the next run is deferred to the next in-window instant.

RecordingProbeExecutor

The wire-level IProbeExecutor — replays a probe's saved BowireRecording through the same protocol plugins the workbench uses. Each step is dispatched via InvokeAsync(string, string, string, List<string>, bool, Dictionary<string, string>?, CancellationToken); the last step's status + response become the probe's result and the summed plugin-reported durations its latency. A missing plugin or a transport failure surfaces as ProbeExecutionException so the runner records an Error outcome.

SignalEvent

The payload a signaler receives — which probe, what happened, and the outcome that triggered it.

SignalerConfigException

Thrown by an ISignalerFactory when the --signal argument is invalid.

SignalerException

Thrown by an ISignaler when delivery fails. The runner catches this (and only this) so a bad channel is logged and skipped without aborting the run — a general exception (a bug in the signaler) still propagates.

SignalerRegistry

Discovers the installed ISignalerFactory contributions and resolves a --signal <scheme>:<arg> spec to a live ISignaler. Signaler packages are opt-in siblings; a scheme with no installed package resolves to a clear "install …" error rather than a crash. Same assembly-scan shape the protocol / CLI-command registries use.

TimeProviderProbeScheduler

The Core scheduler: one TimeProvider-backed loop per probe. Building on TimeProvider (not DateTime.UtcNow + Task.Delay()) makes cadence, drift, and lazy-start resume drivable from a fake clock in tests. Each iteration reads the probe's last ledger row, computes the delay to its next run via ProbeSchedule, waits, then runs it once through the ProbeRunner.

TransitionDetector

Decides whether a probe crossed the pass↔fail line (#102). A signal fires only on a transition, never on every run, so an outage pages once and a recovery clears once. The "previous" state is read from the ledger's last row (Decision 2), so transition state survives a restart — a restart mid-outage neither re-fires "went critical" nor drops "recovered".

Interfaces

IProbeExecutor

Executes a probe's saved recording and returns the raw response. The concrete implementation replays the recording through the invoke path; the seam keeps the scheduler + runner testable without a live target.

IProbeScheduler

Drives probe runs on their schedule (#102, Decision 1). The default implementation is TimeProviderProbeScheduler — a hand-rolled TimeProvider loop, no Quartz. A Quartz-grade scheduler arrives as an optional sibling package contributing this interface.

ISignaler

Fires when a probe crosses the pass↔fail line (#102). Implementations are the outbound integrations — Slack / PagerDuty / OTLP-logs — each shipping as a separate opt-in package and registered only when the operator passes the matching --signal flag. Core Monitoring registers no signaler, so nothing leaves the host by default.

ISignalerFactory

Builds an ISignaler from a --signal <scheme>:<arg> spec. Each outbound channel ships as its own opt-in sibling package (…Monitoring.Slack, …Monitoring.PagerDuty, …Monitoring.Otlp) contributing one factory, so Core + the Core-adjacent Monitoring package gain no third-party dependencies. The CLI discovers factories by assembly scan and reports a clear "install the package" message when a scheme has no factory. Implementations must be zero-config (parameterless ctor) so the registry can Activator.CreateInstance them.

Enums

ProbeAssertionKind

The kinds of predicate v1 supports.

ProbeResult

Outcome of a single probe run.

ProbeSeverity

Severity of a probe's failure — maps onto the signaler's routing.

ProbeTransition

The pass↔fail edge a run produced.