feat(audit): ScriptTrace channel + RouteCall/ScriptRun/AttributeWrite/AttributeWait kinds
Flow Debugger (design 2026-09-05 §5.1): trace steps are audit rows. New AuditChannel.ScriptTrace appended after Cluster and four kinds appended after ManualFailover so stored ordinals never move; lock-in tests + alog.md §4 updated. Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
This commit is contained in:
@@ -135,7 +135,7 @@ Single wide table, polymorphic by `Channel` + `Kind` discriminators, JSON payloa
|
||||
- `IX_AuditLog_Target_Occurred (Target, OccurredAtUtc)` — "what did we send to system X."
|
||||
- Partitioning by month on `OccurredAtUtc` from day one (purge becomes a partition switch instead of a delete storm).
|
||||
|
||||
**`Kind` values (flat — 17 discriminators across all channels; pinned by `AuditEnumTests`):**
|
||||
**`Kind` values (flat — 21 discriminators across all channels; pinned by `AuditEnumTests`):**
|
||||
|
||||
| Kind | Fires when |
|
||||
|---|---|
|
||||
@@ -156,6 +156,10 @@ Single wide table, polymorphic by `Channel` + `Kind` discriminators, JSON payloa
|
||||
| `SecuredWriteExpire` | A `Pending` secured write aged past its server-side TTL and was transitioned to `Expired` without executing — emitted by the system (no verifier). |
|
||||
| `ReconciliationAbandoned` | A reconciliation pull row failed to insert up to the permanent-abandon threshold and central advanced its cursor past it; one synthetic row so the loss is queryable in the Audit Log itself. |
|
||||
| `ManualFailover` | An administrator triggered a manual failover of the central pair from the Health page; one row per invocation, written BEFORE the graceful `Cluster.Leave` is issued. `Target` = the leaving node's address. |
|
||||
| `RouteCall` | Central `Route.To(...).Call(...)` round-trip completed (success or failure). **Always-on** — one row per routed call regardless of tracing; `Extra.childExecutionId` = the site run's `ExecutionId`. Channel `ScriptTrace`. (Flow Debugger, 2026-09-05) |
|
||||
| `ScriptRun` | A traced site script run completed/failed/timed out; `Extra.trigger*` carries the trigger for value-change runs. Channel `ScriptTrace`, traced executions only. |
|
||||
| `AttributeWrite` | A traced script wrote an attribute (or a batch); `Extra.writes[]` lists each write's outcome. Channel `ScriptTrace`, traced executions only. |
|
||||
| `AttributeWait` | A traced script's `WaitAsync`/`WaitForAsync` resolved (satisfied / timeout / cancelled / error) with the observed value. Channel `ScriptTrace`, traced executions only. |
|
||||
|
||||
### Site: `AuditLog` (SQLite)
|
||||
|
||||
|
||||
@@ -21,5 +21,13 @@ public enum AuditChannel
|
||||
/// restarted the active node, which is exactly the kind of act an audit log exists to
|
||||
/// attribute. (decision 2026-07-22)
|
||||
/// </summary>
|
||||
Cluster
|
||||
Cluster,
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger trace rows (design 2026-09-05): a script run, an attribute write, an
|
||||
/// attribute wait or a central route call, emitted only for traced executions
|
||||
/// (plus the always-on <see cref="AuditKind.RouteCall"/>). Appended last so stored
|
||||
/// ordinals of existing channels never move.
|
||||
/// </summary>
|
||||
ScriptTrace
|
||||
}
|
||||
|
||||
@@ -49,5 +49,17 @@ public enum AuditKind
|
||||
/// is issued (the acting node is usually not the one that goes away, but the audit must
|
||||
/// survive either outcome). (decision 2026-07-22)
|
||||
/// </summary>
|
||||
ManualFailover
|
||||
ManualFailover,
|
||||
|
||||
/// <summary>Central <c>Route.To().Call</c> round-trip (always-on; <see cref="AuditChannel.ScriptTrace"/> channel).</summary>
|
||||
RouteCall,
|
||||
|
||||
/// <summary>One site script run completed/failed/timed out (traced executions only; <see cref="AuditChannel.ScriptTrace"/>).</summary>
|
||||
ScriptRun,
|
||||
|
||||
/// <summary>One attribute write or batch write made by a traced script (traced executions only; <see cref="AuditChannel.ScriptTrace"/>).</summary>
|
||||
AttributeWrite,
|
||||
|
||||
/// <summary>One <c>WaitAsync</c>/<c>WaitForAsync</c> outcome in a traced script (traced executions only; <see cref="AuditChannel.ScriptTrace"/>).</summary>
|
||||
AttributeWait
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Flow Debugger (design 2026-09-05): the <c>ScriptTrace</c> channel and its four kinds must
|
||||
/// fit the central <c>AuditLog.Category</c>/<c>Kind</c> columns and never move existing ordinals.
|
||||
/// </summary>
|
||||
public class ScriptTraceEnumTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(AuditKind.RouteCall)]
|
||||
[InlineData(AuditKind.ScriptRun)]
|
||||
[InlineData(AuditKind.AttributeWrite)]
|
||||
[InlineData(AuditKind.AttributeWait)]
|
||||
public void ScriptTraceKinds_FitTheCentralColumns(AuditKind kind)
|
||||
{
|
||||
// AuditLog.Category / Kind are nvarchar(32) (AuditLogEntityTypeConfiguration).
|
||||
Assert.True(kind.ToString().Length <= 32);
|
||||
Assert.True(AuditChannel.ScriptTrace.ToString().Length <= 32);
|
||||
Assert.Equal($"ScriptTrace.{kind}", AuditFieldBuilders.BuildAction(AuditChannel.ScriptTrace, kind));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScriptTrace_IsAppendedAfterCluster_SoExistingOrdinalsAreStable()
|
||||
{
|
||||
Assert.True((int)AuditChannel.ScriptTrace > (int)AuditChannel.Cluster);
|
||||
Assert.True((int)AuditKind.RouteCall > (int)AuditKind.ManualFailover);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ public class AuditEnumTests
|
||||
[Fact]
|
||||
public void AuditChannel_HasExactlyExpectedMembers()
|
||||
{
|
||||
var expected = new[] { "ApiOutbound", "DbOutbound", "Notification", "ApiInbound", "SecuredWrite", "Cluster" };
|
||||
var expected = new[] { "ApiOutbound", "DbOutbound", "Notification", "ApiInbound", "SecuredWrite", "Cluster", "ScriptTrace" };
|
||||
var actual = Enum.GetValues(typeof(AuditChannel))
|
||||
.Cast<AuditChannel>()
|
||||
.Select(x => x.ToString())
|
||||
@@ -23,7 +23,7 @@ public class AuditEnumTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuditKind_HasExactlySeventeenExpectedMembers()
|
||||
public void AuditKind_HasExactlyTwentyOneExpectedMembers()
|
||||
{
|
||||
var expected = new[]
|
||||
{
|
||||
@@ -34,13 +34,14 @@ public class AuditEnumTests
|
||||
"SecuredWriteExpire",
|
||||
"ReconciliationAbandoned",
|
||||
"ManualFailover",
|
||||
"RouteCall", "ScriptRun", "AttributeWrite", "AttributeWait",
|
||||
};
|
||||
var actual = Enum.GetValues(typeof(AuditKind))
|
||||
.Cast<AuditKind>()
|
||||
.Select(x => x.ToString())
|
||||
.ToArray();
|
||||
|
||||
Assert.Equal(17, actual.Length);
|
||||
Assert.Equal(21, actual.Length);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user