feat(sitecall-audit): add SourceNode to SiteCallOperational + SiteCall entity
This commit is contained in:
@@ -162,6 +162,9 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
|
|||||||
Channel: context.Channel,
|
Channel: context.Channel,
|
||||||
Target: context.Target,
|
Target: context.Target,
|
||||||
SourceSite: context.SourceSite,
|
SourceSite: context.SourceSite,
|
||||||
|
// SourceNode: stamped by Task 14 once the bridge gets an
|
||||||
|
// INodeIdentityProvider; null until then.
|
||||||
|
SourceNode: null,
|
||||||
Status: operationalStatus,
|
Status: operationalStatus,
|
||||||
RetryCount: context.RetryCount,
|
RetryCount: context.RetryCount,
|
||||||
LastError: lastError,
|
LastError: lastError,
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ public sealed record SiteCall
|
|||||||
/// <summary>Site id that submitted the cached call.</summary>
|
/// <summary>Site id that submitted the cached call.</summary>
|
||||||
public required string SourceSite { get; init; }
|
public required string SourceSite { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cluster node on which the cached call was emitted — <c>node-a</c> /
|
||||||
|
/// <c>node-b</c> for site rows (qualified by <see cref="SourceSite"/>),
|
||||||
|
/// <c>central-a</c> / <c>central-b</c> for central-originated rows. Stamped
|
||||||
|
/// by the emitting node from <c>INodeIdentityProvider</c>; nullable so
|
||||||
|
/// reconciled rows from a node that has since been retired don't block ingest.
|
||||||
|
/// </summary>
|
||||||
|
public string? SourceNode { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lifecycle status — string form of
|
/// Lifecycle status — string form of
|
||||||
/// <see cref="ScadaLink.Commons.Types.Enums.AuditStatus"/>. Monotonic: later rank
|
/// <see cref="ScadaLink.Commons.Types.Enums.AuditStatus"/>. Monotonic: later rank
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ namespace ScadaLink.Commons.Types;
|
|||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="Target">Human-readable target (e.g. <c>"ERP.GetOrder"</c>).</param>
|
/// <param name="Target">Human-readable target (e.g. <c>"ERP.GetOrder"</c>).</param>
|
||||||
/// <param name="SourceSite">Site id that submitted the cached call.</param>
|
/// <param name="SourceSite">Site id that submitted the cached call.</param>
|
||||||
|
/// <param name="SourceNode">
|
||||||
|
/// The cluster node on which the cached call was emitted — <c>node-a</c> / <c>node-b</c>
|
||||||
|
/// for site rows (qualified by <paramref name="SourceSite"/>), <c>central-a</c> /
|
||||||
|
/// <c>central-b</c> for central-originated rows. Stamped by the emitting node from
|
||||||
|
/// <c>INodeIdentityProvider</c>; nullable so reconciled rows from a node that has since
|
||||||
|
/// been retired don't block ingest.
|
||||||
|
/// </param>
|
||||||
/// <param name="Status">
|
/// <param name="Status">
|
||||||
/// Lifecycle status — string form of <see cref="ScadaLink.Commons.Types.Enums.AuditStatus"/>:
|
/// Lifecycle status — string form of <see cref="ScadaLink.Commons.Types.Enums.AuditStatus"/>:
|
||||||
/// <c>Submitted</c>, <c>Retrying</c>, <c>Attempted</c>, <c>Delivered</c>,
|
/// <c>Submitted</c>, <c>Retrying</c>, <c>Attempted</c>, <c>Delivered</c>,
|
||||||
@@ -37,6 +44,7 @@ public sealed record SiteCallOperational(
|
|||||||
string Channel,
|
string Channel,
|
||||||
string Target,
|
string Target,
|
||||||
string SourceSite,
|
string SourceSite,
|
||||||
|
string? SourceNode,
|
||||||
string Status,
|
string Status,
|
||||||
int RetryCount,
|
int RetryCount,
|
||||||
string? LastError,
|
string? LastError,
|
||||||
|
|||||||
@@ -648,6 +648,9 @@ public class ScriptRuntimeContext
|
|||||||
Channel: "ApiOutbound",
|
Channel: "ApiOutbound",
|
||||||
Target: target,
|
Target: target,
|
||||||
SourceSite: _siteId,
|
SourceSite: _siteId,
|
||||||
|
// SourceNode: stamped by Task 14 once the script context
|
||||||
|
// gets an INodeIdentityProvider; null until then.
|
||||||
|
SourceNode: null,
|
||||||
Status: "Submitted",
|
Status: "Submitted",
|
||||||
RetryCount: 0,
|
RetryCount: 0,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
@@ -766,6 +769,9 @@ public class ScriptRuntimeContext
|
|||||||
Channel: "ApiOutbound",
|
Channel: "ApiOutbound",
|
||||||
Target: target,
|
Target: target,
|
||||||
SourceSite: _siteId,
|
SourceSite: _siteId,
|
||||||
|
// SourceNode: stamped by Task 14 once the script context
|
||||||
|
// gets an INodeIdentityProvider; null until then.
|
||||||
|
SourceNode: null,
|
||||||
Status: "Attempted",
|
Status: "Attempted",
|
||||||
// RetryCount stays 0 — the operation never reached the
|
// RetryCount stays 0 — the operation never reached the
|
||||||
// S&F retry sweep, so no retries were performed.
|
// S&F retry sweep, so no retries were performed.
|
||||||
@@ -833,6 +839,9 @@ public class ScriptRuntimeContext
|
|||||||
Channel: "ApiOutbound",
|
Channel: "ApiOutbound",
|
||||||
Target: target,
|
Target: target,
|
||||||
SourceSite: _siteId,
|
SourceSite: _siteId,
|
||||||
|
// SourceNode: stamped by Task 14 once the script context
|
||||||
|
// gets an INodeIdentityProvider; null until then.
|
||||||
|
SourceNode: null,
|
||||||
Status: operationalTerminalStatus,
|
Status: operationalTerminalStatus,
|
||||||
RetryCount: 0,
|
RetryCount: 0,
|
||||||
LastError: result.Success ? null : result.ErrorMessage,
|
LastError: result.Success ? null : result.ErrorMessage,
|
||||||
@@ -1243,6 +1252,9 @@ public class ScriptRuntimeContext
|
|||||||
Channel: "DbOutbound",
|
Channel: "DbOutbound",
|
||||||
Target: target,
|
Target: target,
|
||||||
SourceSite: _siteId,
|
SourceSite: _siteId,
|
||||||
|
// SourceNode: stamped by Task 14 once the script context
|
||||||
|
// gets an INodeIdentityProvider; null until then.
|
||||||
|
SourceNode: null,
|
||||||
Status: "Submitted",
|
Status: "Submitted",
|
||||||
RetryCount: 0,
|
RetryCount: 0,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMigr
|
|||||||
Channel: "ApiOutbound",
|
Channel: "ApiOutbound",
|
||||||
Target: target,
|
Target: target,
|
||||||
SourceSite: siteId,
|
SourceSite: siteId,
|
||||||
|
SourceNode: null,
|
||||||
Status: "Submitted",
|
Status: "Submitted",
|
||||||
RetryCount: 0,
|
RetryCount: 0,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public class CachedWriteCombinedTelemetryTests : TestKit, IClassFixture<MsSqlMig
|
|||||||
Channel: "DbOutbound",
|
Channel: "DbOutbound",
|
||||||
Target: target,
|
Target: target,
|
||||||
SourceSite: siteId,
|
SourceSite: siteId,
|
||||||
|
SourceNode: null,
|
||||||
Status: "Submitted",
|
Status: "Submitted",
|
||||||
RetryCount: 0,
|
RetryCount: 0,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class CachedCallTelemetryForwarderTests
|
|||||||
Channel: "ApiOutbound",
|
Channel: "ApiOutbound",
|
||||||
Target: "ERP.GetOrder",
|
Target: "ERP.GetOrder",
|
||||||
SourceSite: "site-1",
|
SourceSite: "site-1",
|
||||||
|
SourceNode: null,
|
||||||
Status: "Submitted",
|
Status: "Submitted",
|
||||||
RetryCount: 0,
|
RetryCount: 0,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
@@ -80,6 +81,7 @@ public class CachedCallTelemetryForwarderTests
|
|||||||
Channel: "ApiOutbound",
|
Channel: "ApiOutbound",
|
||||||
Target: "ERP.GetOrder",
|
Target: "ERP.GetOrder",
|
||||||
SourceSite: "site-1",
|
SourceSite: "site-1",
|
||||||
|
SourceNode: null,
|
||||||
Status: "Attempted",
|
Status: "Attempted",
|
||||||
RetryCount: retryCount,
|
RetryCount: retryCount,
|
||||||
LastError: lastError,
|
LastError: lastError,
|
||||||
@@ -107,6 +109,7 @@ public class CachedCallTelemetryForwarderTests
|
|||||||
Channel: "ApiOutbound",
|
Channel: "ApiOutbound",
|
||||||
Target: "ERP.GetOrder",
|
Target: "ERP.GetOrder",
|
||||||
SourceSite: "site-1",
|
SourceSite: "site-1",
|
||||||
|
SourceNode: null,
|
||||||
Status: status,
|
Status: status,
|
||||||
RetryCount: 2,
|
RetryCount: 2,
|
||||||
LastError: null,
|
LastError: null,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using ScadaLink.Commons.Entities.Audit;
|
||||||
|
using ScadaLink.Commons.Types;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests.Entities.Audit;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies the <see cref="SiteCall"/> central operational entity carries the
|
||||||
|
/// SourceNode column (additive, nullable) through init-only construction and
|
||||||
|
/// <c>with</c> expressions. Sibling to <see cref="AuditEventTests"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class SiteCallTests
|
||||||
|
{
|
||||||
|
private static SiteCall MinimalRow() => new()
|
||||||
|
{
|
||||||
|
TrackedOperationId = TrackedOperationId.New(),
|
||||||
|
Channel = "ApiOutbound",
|
||||||
|
Target = "ERP.GetOrder",
|
||||||
|
SourceSite = "site-01",
|
||||||
|
Status = "Submitted",
|
||||||
|
RetryCount = 0,
|
||||||
|
CreatedAtUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc),
|
||||||
|
UpdatedAtUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc),
|
||||||
|
IngestedAtUtc = new DateTime(2026, 5, 23, 12, 0, 1, DateTimeKind.Utc),
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SiteCall_carries_SourceNode()
|
||||||
|
{
|
||||||
|
// SourceNode identifies the cluster node that emitted the cached call
|
||||||
|
// (site node-a/node-b or central-a/central-b). Additive nullable init
|
||||||
|
// property — defaults to null on rows ingested before the column
|
||||||
|
// existed, and round-trips its value via `with` expressions.
|
||||||
|
var row = MinimalRow();
|
||||||
|
Assert.Null(row.SourceNode);
|
||||||
|
|
||||||
|
var stamped = row with { SourceNode = "node-b" };
|
||||||
|
Assert.Equal("node-b", stamped.SourceNode);
|
||||||
|
Assert.Null(row.SourceNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,10 @@ public class CachedCallTelemetryTests
|
|||||||
Channel: nameof(AuditChannel.ApiOutbound),
|
Channel: nameof(AuditChannel.ApiOutbound),
|
||||||
Target: "ERP.GetOrder",
|
Target: "ERP.GetOrder",
|
||||||
SourceSite: SiteId,
|
SourceSite: SiteId,
|
||||||
|
// SourceNode: actual stamping arrives with Task 14; for now the
|
||||||
|
// packet builder leaves the column null so existing assertions on
|
||||||
|
// the packet's other fields stay intact.
|
||||||
|
SourceNode: null,
|
||||||
Status: status.ToString(),
|
Status: status.ToString(),
|
||||||
RetryCount: retryCount,
|
RetryCount: retryCount,
|
||||||
LastError: lastError,
|
LastError: lastError,
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using ScadaLink.Commons.Types;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests.Types;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies <see cref="SiteCallOperational"/> — the positional record carried on
|
||||||
|
/// the combined <c>CachedCallTelemetry</c> packet — round-trips the SourceNode
|
||||||
|
/// field through positional construction (where the parameter sits between
|
||||||
|
/// <c>SourceSite</c> and <c>Status</c>, mirroring the central <c>SiteCalls</c>
|
||||||
|
/// table column order).
|
||||||
|
/// </summary>
|
||||||
|
public class SiteCallOperationalTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SiteCallOperational_carries_SourceNode()
|
||||||
|
{
|
||||||
|
// SourceNode identifies the cluster node that emitted the cached call
|
||||||
|
// (site node-a/node-b or central-a/central-b). Nullable — callsites
|
||||||
|
// pass null until INodeIdentityProvider stamping arrives in Task 14.
|
||||||
|
var trackedId = TrackedOperationId.New();
|
||||||
|
var nowUtc = new DateTime(2026, 5, 23, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
var defaulted = new SiteCallOperational(
|
||||||
|
TrackedOperationId: trackedId,
|
||||||
|
Channel: "ApiOutbound",
|
||||||
|
Target: "ERP.GetOrder",
|
||||||
|
SourceSite: "site-01",
|
||||||
|
SourceNode: null,
|
||||||
|
Status: "Submitted",
|
||||||
|
RetryCount: 0,
|
||||||
|
LastError: null,
|
||||||
|
HttpStatus: null,
|
||||||
|
CreatedAtUtc: nowUtc,
|
||||||
|
UpdatedAtUtc: nowUtc,
|
||||||
|
TerminalAtUtc: null);
|
||||||
|
Assert.Null(defaulted.SourceNode);
|
||||||
|
|
||||||
|
var stamped = defaulted with { SourceNode = "node-a" };
|
||||||
|
Assert.Equal("node-a", stamped.SourceNode);
|
||||||
|
Assert.Null(defaulted.SourceNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user