9d2834e30a
The daily site_events retention purge (and the storage-cap trim) is CDC-captured on a replication-enabled site node exactly like any other write — correct by design, since LocalDb Phase 2 deliberately has no purge-exemption path — so the backlog jumps by the deleted batch size at purge time. LocalDbOplogBacklog / localdb_oplog_depth spike, drain, and an operator watching the gauge with no context reads it as a replication fault. Documentation + one log line, no behaviour change: - topology-guide.md gains "Reading the replication backlog — the daily site_events purge burst": when it fires (PurgeInterval 24h, anchored to the active node's PROCESS START, not a wall-clock hour, so it moves after every failover), where it shows (replicated nodes only — not rig site-b/site-c), the healthy signature (LocalDbReplicationConnected stays true, backlog returns to ~0) and what a genuine fault looks like instead. - Component-SiteEventLogging.md Storage records the same under retention/purge; Component-HealthMonitoring.md gains the two previously-undocumented LocalDbReplicationConnected / LocalDbOplogBacklog metric rows carrying the caveat, with cross-references both ways. - EventLogPurgeService emits one Information line naming the row count and the expected transient backlog when a purge deleted rows on a replication-enabled node, so the spike is correlatable in the log. Replication-awareness comes in as a Host-supplied SiteEventLogReplicationCheck delegate, mirroring the existing SiteEventLogActiveNodeCheck seam: SiteLocalDbSetup.ReplicationIsConfigured goes internal so the PeerAddress-OR-ApiKey rule stays in one place and SiteEventLogging never learns to read LocalDb config. Unregistered ⇒ no note, matching the default that replication is opt-in and off. Both delete paths carry the note (a cap trim is usually the larger burst); the predicate is try/caught since a log-wording check must never break the purge. Tests: 5 new EventLogPurgeServiceTests cases (replicated logs it, unreplicated does not, zero-rows does not, cap purge logs it, throwing predicate still purges and swallows) via a local capturing ILogger. SiteEventLogging 81/81 green, Host 490/490 green, full solution build clean (0 warnings).
144 lines
11 KiB
Markdown
144 lines
11 KiB
Markdown
# Component: Site Event Logging
|
|
|
|
## Purpose
|
|
|
|
The Site Event Logging component records operational events at each site cluster, providing a local audit trail of runtime activity. Events are queryable from the central UI for remote troubleshooting.
|
|
|
|
## Location
|
|
|
|
Site clusters (event recording and storage). Central cluster (remote query access via UI).
|
|
|
|
## Responsibilities
|
|
|
|
- Record operational events from all site subsystems.
|
|
- Persist events to local SQLite.
|
|
- Enforce 30-day retention policy with automatic purging.
|
|
- Respond to remote queries from central for event log data.
|
|
|
|
## Events Logged
|
|
|
|
| Category | Events |
|
|
|----------|--------|
|
|
| Script Executions | Script started, completed *(sampled/opt-in — see Volume Policy below)*; failed, timed out, recursion limit exceeded (with error details; always logged) |
|
|
| Script Run Summary | Interval aggregate replacing the per-run rows by default — one Info row per flush interval summarizing every instance script run since the last flush (see Volume Policy) |
|
|
| Alarm Events | Alarm activated, alarm cleared (which alarm, which instance), alarm evaluation error |
|
|
| Deployment Events | Configuration received from central, scripts compiled, applied successfully, apply failed |
|
|
| Data Connection Status | Connected, disconnected, reconnected (per connection) |
|
|
| Store-and-Forward | Message queued, delivered, retried, parked |
|
|
| Instance Lifecycle | Instance enabled, disabled, deleted |
|
|
| Notification | Site→central forward failure, long-buffered notification (still in the site buffer past a threshold) |
|
|
|
|
## Volume Policy — Script Run Events (WP3.2, 2026-08-15)
|
|
|
|
Per-run instance-script Started/Completed Info events were, before this policy, the dominant
|
|
`site_events` writer: two rows per run, translating to roughly a dozen physical row-writes
|
|
across a replicated site pair once CDC oplog/row-version rows and their eventual purge
|
|
tombstones are counted — for content nobody looks at unless something went wrong. The policy
|
|
below (design memo: `docs/plans/2026-08-15-site-events-policy-design.md`) keeps the signal
|
|
while removing the volume.
|
|
|
|
- **Off by default.** `ScadaBridge:SiteRuntime:PerRunScriptEvents` (default `false`) is the
|
|
global switch back to the legacy per-run Started/Completed rows for every script.
|
|
- **Interval aggregate instead.** `ScriptRunSummaryRecorder` accumulates per-`(instance,
|
|
script)` counters (started, completed, failed, timed out, total/max duration) from the same
|
|
call sites that used to emit the per-run rows. A hosted flush service emits **one** "script"
|
|
Info row per interval — `ScadaBridge:SiteRuntime:ScriptRunSummaryIntervalSeconds` (default
|
|
`300`, i.e. 5 minutes; `0` disables the flush) — with headline totals in the message (e.g.
|
|
"1,243 runs: 1,240 completed, 2 failed, 1 timed out across 17 scripts") and a per-script
|
|
breakdown in `Details`, capped at the **top 50 scripts by run activity** with an "others"
|
|
rollup entry so a pathological deployment cannot mint an oversized row. **An interval with
|
|
zero activity emits no row** — the standby, which never runs scripts, therefore never
|
|
produces a summary row naturally. Source is `"ScriptRunSummary"`; Instance ID is `NULL`
|
|
(the row spans every instance).
|
|
- **Per-script opt-in for debugging.** `ScadaBridge:SiteRuntime:PerRunScriptEventScripts`
|
|
(default empty) restores the legacy per-run rows for named scripts only, without paying the
|
|
volume cost for the rest of the deployment. Entries are `"{InstanceName}/{ScriptName}"` for
|
|
an exact match or `"{InstanceName}/*"` for every script on that instance. Effective only
|
|
while the global switch is off.
|
|
- **Hot-togglable.** Both keys are read from the live options snapshot at the top of every
|
|
run (`IOptionsMonitor<SiteRuntimeOptions>.CurrentValue`), so an operator can flip either one
|
|
— including narrowing an opt-in to a single misbehaving script — without a restart or a
|
|
redeploy.
|
|
- **Error-level events are unconditional.** Timeout, failure, stuck-watchdog, and
|
|
recursion-limit Error rows are unaffected by any of the above — they always fire, regardless
|
|
of `PerRunScriptEvents` or the opt-in list.
|
|
|
|
## Event Entry Schema
|
|
|
|
Each event entry contains:
|
|
- **Timestamp**: When the event occurred.
|
|
- **Event Type**: Category of the event (script, alarm, deployment, connection, store-and-forward, instance-lifecycle, notification).
|
|
- **Severity**: Info, Warning, or Error.
|
|
- **Instance ID** *(optional)*: The instance associated with the event (if applicable).
|
|
- **Source**: The subsystem that generated the event (e.g., "ScriptActor:MonitorSpeed", "AlarmActor:OverTemp", "DataConnection:PLC1").
|
|
- **Message**: Human-readable description of the event.
|
|
- **Details** *(optional)*: Additional structured data (e.g., exception stack trace, alarm name, message ID, compilation errors).
|
|
|
|
## Storage
|
|
|
|
- Events are stored in the **consolidated site LocalDb** SQLite file (`site_events` table;
|
|
see CLAUDE.md → Consolidated site database). Only the **active node** generates and stores
|
|
events (a standby runs no scripts, subsystems, or deployments to log).
|
|
- **`site_events` IS replicated** (`SiteLocalDbSetup.ReplicatedTables`) on a site pair with
|
|
replication configured — this reverses the component's pre-LocalDb-Phase-1 behavior, and the
|
|
reason is **failover history continuity, not either-node queries**: the query handler
|
|
(`EventLogHandlerActor`) is a **cluster singleton**, so a central query always routes to the
|
|
actively-written copy regardless of which node answers the gRPC call — replication is never
|
|
needed to make queries *work*. What replication buys is **content to read after a failover**:
|
|
when the active node dies, the singleton restarts on the survivor, and its query service reads
|
|
the survivor's *local* copy — which has history *only because it was replicated*. Without
|
|
replication, a failover would start the event log empty at precisely the moment an operator
|
|
is investigating an incident (WP3.2 design memo, Decision (b): `docs/plans/2026-08-15-site-events-policy-design.md`).
|
|
On a site pair running **without** replication configured (by deliberate choice, e.g. the
|
|
rig's site-b/site-c), the log stays node-local and a failover does start it fresh — the
|
|
documented trade of not configuring a peer, unchanged by this policy.
|
|
- **Retention**: 30 days. A **daily background job** runs on the active node and deletes all events older than 30 days. Hard delete — no archival. Deletes are sliced into bounded 1000-row batches per DELETE statement rather than one unbounded statement, so a large expired backlog does not hold the write lock (and every concurrent recorder flush) for the duration of the purge.
|
|
- **Storage cap**: A configurable maximum database size (default: 1 GB) is enforced. If the storage cap is reached before the 30-day retention window, the oldest events are purged first. This prevents disk exhaustion from alarm storms, script failure loops, or connection flapping.
|
|
- **Purge deletes are CDC-captured on a replicated site — the resulting backlog spike is expected.**
|
|
Retention and cap deletes are ordinary row changes on a replicated table; there is deliberately no
|
|
purge-exemption path (LocalDb Phase 2 — CDC does all three jobs, and a table-wide exemption is the
|
|
same mechanism that made `ReplaceAllAsync` unsafe). One oplog row is queued per deleted event, so
|
|
the site health report's `LocalDbOplogBacklog` (Prometheus `localdb_oplog_depth`) jumps by the size
|
|
of the batch at purge time and drains as the peer acks it. **Healthy signature:**
|
|
`LocalDbReplicationConnected` stays true across the spike and the backlog returns to ~0; a backlog
|
|
that keeps climbing, or climbs while `LocalDbReplicationConnected` is false, is a genuine
|
|
replication fault and not the purge. The purge logs an Information line naming the row count and
|
|
the expected transient backlog whenever it deletes rows on a replication-enabled node, so an
|
|
operator can correlate a spike with the purge that caused it — a spike with no such line nearby is
|
|
not the purge. Node-local wiring: the Host supplies the replication predicate
|
|
(`SiteLocalDbSetup.ReplicationIsConfigured` — the `PeerAddress`-OR-`ApiKey` rule) as
|
|
`SiteEventLogReplicationCheck`; this component never reads LocalDb configuration itself, and with
|
|
no predicate registered the note is suppressed. Operator detail —
|
|
including that the purge timer is anchored to the active node's process start, not a wall-clock
|
|
hour — is in `docs/deployment/topology-guide.md` → *Reading the replication backlog*. See also the
|
|
Volume Policy section above for why this residual cost is small now that per-run script rows are
|
|
off by default.
|
|
|
|
## Central Access
|
|
|
|
- The central UI can query site event logs remotely via the Communication Layer.
|
|
- Queries support filtering by:
|
|
- Event type / category
|
|
- Time range
|
|
- Instance ID
|
|
- Severity
|
|
- **Keyword search**: Free-text search on message and source fields (SQLite LIKE query). Useful for finding events by script name, alarm name, or error message across all instances.
|
|
- Results are **paginated** with a configurable page size (default: 500 events). Each response includes a continuation token for fetching additional pages. This prevents broad queries from overwhelming the communication channel.
|
|
- The site processes the query locally against SQLite and returns matching results to central.
|
|
|
|
## Dependencies
|
|
|
|
- **SQLite**: Local storage on each site node.
|
|
- **Communication Layer**: Handles remote query requests from central.
|
|
- **Site Runtime**: Generates script execution events, alarm events, deployment application events, and instance lifecycle events.
|
|
- **Data Connection Layer**: Generates connection status events.
|
|
- **Store-and-Forward Engine**: Generates buffer activity events, including notification-category forward failures and long-buffered notifications on the site→central notification path.
|
|
|
|
## Interactions
|
|
|
|
- **All site subsystems**: Event logging is a cross-cutting concern — any subsystem that produces notable events calls the Event Logging service.
|
|
- **Communication Layer**: Receives remote queries from central and returns results.
|
|
- **Central UI**: Site Event Log Viewer displays queried events.
|
|
- **Store-and-Forward Engine**: Its notification path (the site→central forward of script-generated notifications) reports forward failures and long-buffered notifications as Notification-category events. Routine enqueue and forward-success events are deliberately not logged — central's authoritative `Notifications` table (owned by the Notification Outbox component) is the audit record of record; site-side logging covers only the in-transit blind spot when central is unreachable.
|
|
- **Health Monitoring**: Script error rates and alarm evaluation error rates can be derived from event log data; this component also reports `SiteEventLogWriteFailures`, and its purge is the expected cause of the transient `LocalDbOplogBacklog` spikes described in [Component-HealthMonitoring.md](Component-HealthMonitoring.md) → Monitored Metrics.
|