feat(sitecall-audit): carry + persist SourceNode end-to-end via cached telemetry
Site: site emitters of SiteCallOperational (ExternalSystemClient, the script-API cached call path in ScriptRuntimeContext, CachedCallLifecycleBridge) inject INodeIdentityProvider and stamp SourceNode = NodeName at construction. OperationTrackingStore call site in CachedCallTelemetryForwarder now stamps SourceNode too. Central: SiteCallAuditRepository.UpsertAsync INSERT includes SourceNode in the column list; conditional monotonic UPDATE uses COALESCE(@SourceNode, SourceNode) so later packets cannot blank a previously- stamped value. After this commit every SiteCalls row carries node-a/node-b in SourceNode (subject to monotonic preservation).
This commit is contained in:
@@ -146,7 +146,14 @@ public static class ServiceCollectionExtensions
|
||||
new CachedCallTelemetryForwarder(
|
||||
sp.GetRequiredService<IAuditWriter>(),
|
||||
sp.GetService<ScadaLink.Commons.Interfaces.IOperationTrackingStore>(),
|
||||
sp.GetRequiredService<ILogger<CachedCallTelemetryForwarder>>()));
|
||||
sp.GetRequiredService<ILogger<CachedCallTelemetryForwarder>>(),
|
||||
// SourceNode-stamping (Task 14): the local node identity is
|
||||
// threaded through so RecordEnqueueAsync can stamp the
|
||||
// tracking row's SourceNode column. GetService — central
|
||||
// composition roots may not register the provider, in which
|
||||
// case the forwarder degrades to a null SourceNode rather
|
||||
// than failing the DI resolution.
|
||||
sp.GetService<INodeIdentityProvider>()));
|
||||
|
||||
// M3 Bundle F: bridge the store-and-forward retry-loop observer hook
|
||||
// to the cached-call forwarder so per-attempt + terminal telemetry
|
||||
@@ -154,7 +161,17 @@ public static class ServiceCollectionExtensions
|
||||
// as the script-thread CachedSubmit row. Registered as a singleton
|
||||
// and also bound to ICachedCallLifecycleObserver so AddStoreAndForward
|
||||
// can resolve it through DI (Bundle F StoreAndForward wiring change).
|
||||
services.AddSingleton<CachedCallLifecycleBridge>();
|
||||
// SourceNode-stamping (Task 14): factory-resolved so the
|
||||
// INodeIdentityProvider singleton can be threaded through — the
|
||||
// bridge stamps SiteCallOperational.SourceNode from
|
||||
// INodeIdentityProvider.NodeName on every cached-call lifecycle row.
|
||||
// GetService (not GetRequiredService) — central composition roots may
|
||||
// not register the provider, in which case the bridge degrades to a
|
||||
// null SourceNode rather than failing the DI resolution.
|
||||
services.AddSingleton<CachedCallLifecycleBridge>(sp => new CachedCallLifecycleBridge(
|
||||
sp.GetRequiredService<ICachedCallTelemetryForwarder>(),
|
||||
sp.GetRequiredService<ILogger<CachedCallLifecycleBridge>>(),
|
||||
sp.GetService<INodeIdentityProvider>()));
|
||||
services.AddSingleton<ICachedCallLifecycleObserver>(
|
||||
sp => sp.GetRequiredService<CachedCallLifecycleBridge>());
|
||||
|
||||
|
||||
@@ -39,12 +39,23 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
|
||||
private readonly ICachedCallTelemetryForwarder _forwarder;
|
||||
private readonly ILogger<CachedCallLifecycleBridge> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// SourceNode-stamping (Task 14): the local node identity provider used to
|
||||
/// stamp <c>SiteCallOperational.SourceNode</c> on every cached-call
|
||||
/// lifecycle row this bridge emits. Optional — when null (legacy hosts /
|
||||
/// tests that don't register the provider) SourceNode stays null and
|
||||
/// central persists the <c>SiteCalls</c> row with SourceNode NULL.
|
||||
/// </summary>
|
||||
private readonly INodeIdentityProvider? _nodeIdentity;
|
||||
|
||||
public CachedCallLifecycleBridge(
|
||||
ICachedCallTelemetryForwarder forwarder,
|
||||
ILogger<CachedCallLifecycleBridge> logger)
|
||||
ILogger<CachedCallLifecycleBridge> logger,
|
||||
INodeIdentityProvider? nodeIdentity = null)
|
||||
{
|
||||
_forwarder = forwarder ?? throw new ArgumentNullException(nameof(forwarder));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_nodeIdentity = nodeIdentity;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -114,7 +125,7 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
|
||||
await _forwarder.ForwardAsync(packet, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static CachedCallTelemetry BuildPacket(
|
||||
private CachedCallTelemetry BuildPacket(
|
||||
CachedCallAttemptContext context,
|
||||
AuditKind kind,
|
||||
AuditStatus status,
|
||||
@@ -162,9 +173,11 @@ public sealed class CachedCallLifecycleBridge : ICachedCallLifecycleObserver
|
||||
Channel: context.Channel,
|
||||
Target: context.Target,
|
||||
SourceSite: context.SourceSite,
|
||||
// SourceNode: stamped by Task 14 once the bridge gets an
|
||||
// INodeIdentityProvider; null until then.
|
||||
SourceNode: null,
|
||||
// SourceNode-stamping (Task 14): the local cluster node name
|
||||
// (node-a/node-b on a site). Stamped from the injected
|
||||
// INodeIdentityProvider; null when no provider was wired so
|
||||
// central persists SiteCalls.SourceNode as NULL.
|
||||
SourceNode: _nodeIdentity?.NodeName,
|
||||
Status: operationalStatus,
|
||||
RetryCount: context.RetryCount,
|
||||
LastError: lastError,
|
||||
|
||||
@@ -53,6 +53,14 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
|
||||
private readonly IOperationTrackingStore? _trackingStore;
|
||||
private readonly ILogger<CachedCallTelemetryForwarder> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// SourceNode-stamping (Task 14): local node identity provider used to
|
||||
/// stamp the tracking-store row's <c>SourceNode</c> column on
|
||||
/// <c>RecordEnqueueAsync</c>. Optional — when null (legacy / test hosts)
|
||||
/// the column stays NULL on the tracking row.
|
||||
/// </summary>
|
||||
private readonly INodeIdentityProvider? _nodeIdentity;
|
||||
|
||||
/// <summary>
|
||||
/// Construct the forwarder. <paramref name="trackingStore"/> is optional —
|
||||
/// when null only the audit half of the packet is emitted, which matches
|
||||
@@ -65,11 +73,13 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
|
||||
public CachedCallTelemetryForwarder(
|
||||
IAuditWriter auditWriter,
|
||||
IOperationTrackingStore? trackingStore,
|
||||
ILogger<CachedCallTelemetryForwarder> logger)
|
||||
ILogger<CachedCallTelemetryForwarder> logger,
|
||||
INodeIdentityProvider? nodeIdentity = null)
|
||||
{
|
||||
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_trackingStore = trackingStore;
|
||||
_nodeIdentity = nodeIdentity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -128,16 +138,17 @@ public sealed class CachedCallTelemetryForwarder : ICachedCallTelemetryForwarder
|
||||
// Enqueue — insert-if-not-exists with the operational
|
||||
// channel as the kind discriminator. RetryCount is fixed
|
||||
// at 0 by the tracking store's INSERT contract.
|
||||
// sourceNode plumbed through but left null here; stamping
|
||||
// is wired in a later task (Task 14) once the
|
||||
// INodeIdentityProvider is threaded into the forwarder.
|
||||
// SourceNode-stamping (Task 14): stamp the local node
|
||||
// name (node-a/node-b) from the injected
|
||||
// INodeIdentityProvider; null when no provider was wired
|
||||
// so the tracking row's SourceNode column stays NULL.
|
||||
await _trackingStore.RecordEnqueueAsync(
|
||||
telemetry.Operational.TrackedOperationId,
|
||||
telemetry.Operational.Channel,
|
||||
telemetry.Operational.Target,
|
||||
telemetry.Audit.SourceInstanceId,
|
||||
telemetry.Audit.SourceScript,
|
||||
sourceNode: null,
|
||||
sourceNode: _nodeIdentity?.NodeName,
|
||||
ct).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user