feat(auditlog): site script-side emitters stamp ExecutionId
Move the per-script-execution Guid on ScriptRuntimeContext from _auditCorrelationId to _executionId, and stamp it into the dedicated AuditEvent.ExecutionId column on every script-side audit row: - Sync ApiCall / DbWrite: ExecutionId set; CorrelationId reverts to null (a sync one-shot call has no operation lifecycle). - Cached-call script-side rows (CachedSubmit, immediate-completion ApiCallCached + CachedResolve) and NotifySend: ExecutionId set; CorrelationId unchanged (per-operation TrackedOperationId / NotificationId). Renames the threaded ctor param/field across ExternalSystemHelper, DatabaseHelper, AuditingDbConnection and AuditingDbCommand, and threads the id through NotifyHelper/NotifyTarget. The S&F retry-loop cached rows (CachedCallLifecycleBridge) are out of scope here.
This commit is contained in:
@@ -37,11 +37,11 @@ internal sealed class AuditingDbCommand : DbCommand
|
|||||||
private readonly string _siteId;
|
private readonly string _siteId;
|
||||||
private readonly string _instanceName;
|
private readonly string _instanceName;
|
||||||
private readonly string? _sourceScript;
|
private readonly string? _sourceScript;
|
||||||
private readonly Guid _auditCorrelationId;
|
private readonly Guid _executionId;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private DbConnection? _wrappingConnection;
|
private DbConnection? _wrappingConnection;
|
||||||
|
|
||||||
// Parameter ordering: auditCorrelationId sits immediately after the ILogger,
|
// Parameter ordering: executionId sits immediately after the ILogger,
|
||||||
// consistent with the other three audit-threaded ctors (ExternalSystemHelper,
|
// consistent with the other three audit-threaded ctors (ExternalSystemHelper,
|
||||||
// DatabaseHelper, AuditingDbConnection).
|
// DatabaseHelper, AuditingDbConnection).
|
||||||
public AuditingDbCommand(
|
public AuditingDbCommand(
|
||||||
@@ -52,7 +52,7 @@ internal sealed class AuditingDbCommand : DbCommand
|
|||||||
string instanceName,
|
string instanceName,
|
||||||
string? sourceScript,
|
string? sourceScript,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Guid auditCorrelationId)
|
Guid executionId)
|
||||||
{
|
{
|
||||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||||
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
|
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
|
||||||
@@ -61,7 +61,7 @@ internal sealed class AuditingDbCommand : DbCommand
|
|||||||
_instanceName = instanceName ?? string.Empty;
|
_instanceName = instanceName ?? string.Empty;
|
||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_auditCorrelationId = auditCorrelationId;
|
_executionId = executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Forwarded surface ------------------------------------------------
|
// -- Forwarded surface ------------------------------------------------
|
||||||
@@ -432,10 +432,12 @@ internal sealed class AuditingDbCommand : DbCommand
|
|||||||
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
||||||
Channel = AuditChannel.DbOutbound,
|
Channel = AuditChannel.DbOutbound,
|
||||||
Kind = AuditKind.DbWrite,
|
Kind = AuditKind.DbWrite,
|
||||||
// Audit Log #23: the execution-wide correlation id, so this sync
|
// Audit Log #23: a sync one-shot DB write has no operation
|
||||||
// DbWrite row shares an id with the other sync trust-boundary rows
|
// lifecycle, so CorrelationId is null. ExecutionId carries the
|
||||||
// from the same script run.
|
// per-execution id so this row shares an id with the other sync
|
||||||
CorrelationId = _auditCorrelationId,
|
// trust-boundary rows from the same script run.
|
||||||
|
CorrelationId = null,
|
||||||
|
ExecutionId = _executionId,
|
||||||
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||||
SourceInstanceId = _instanceName,
|
SourceInstanceId = _instanceName,
|
||||||
SourceScript = _sourceScript,
|
SourceScript = _sourceScript,
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ internal sealed class AuditingDbConnection : DbConnection
|
|||||||
private readonly string _siteId;
|
private readonly string _siteId;
|
||||||
private readonly string _instanceName;
|
private readonly string _instanceName;
|
||||||
private readonly string? _sourceScript;
|
private readonly string? _sourceScript;
|
||||||
private readonly Guid _auditCorrelationId;
|
private readonly Guid _executionId;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
// Parameter ordering: auditCorrelationId sits immediately after the ILogger,
|
// Parameter ordering: executionId sits immediately after the ILogger,
|
||||||
// consistent with the other three audit-threaded ctors (ExternalSystemHelper,
|
// consistent with the other three audit-threaded ctors (ExternalSystemHelper,
|
||||||
// DatabaseHelper, AuditingDbCommand).
|
// DatabaseHelper, AuditingDbCommand).
|
||||||
public AuditingDbConnection(
|
public AuditingDbConnection(
|
||||||
@@ -50,7 +50,7 @@ internal sealed class AuditingDbConnection : DbConnection
|
|||||||
string instanceName,
|
string instanceName,
|
||||||
string? sourceScript,
|
string? sourceScript,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Guid auditCorrelationId)
|
Guid executionId)
|
||||||
{
|
{
|
||||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||||
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
|
_auditWriter = auditWriter ?? throw new ArgumentNullException(nameof(auditWriter));
|
||||||
@@ -59,7 +59,7 @@ internal sealed class AuditingDbConnection : DbConnection
|
|||||||
_instanceName = instanceName ?? string.Empty;
|
_instanceName = instanceName ?? string.Empty;
|
||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_auditCorrelationId = auditCorrelationId;
|
_executionId = executionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConnectionString is settable on DbConnection — forward both halves.
|
// ConnectionString is settable on DbConnection — forward both halves.
|
||||||
@@ -99,7 +99,7 @@ internal sealed class AuditingDbConnection : DbConnection
|
|||||||
_instanceName,
|
_instanceName,
|
||||||
_sourceScript,
|
_sourceScript,
|
||||||
_logger,
|
_logger,
|
||||||
_auditCorrelationId);
|
_executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -106,19 +106,22 @@ public class ScriptRuntimeContext
|
|||||||
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23: the execution-wide audit correlation id. Every sync
|
/// Audit Log #23: the per-execution id for this script run. Every
|
||||||
/// trust-boundary audit row emitted by this script execution
|
/// trust-boundary audit row emitted by this script execution
|
||||||
/// (<c>ApiCall</c>, <c>DbWrite</c>) is stamped with this id so all the
|
/// (sync <c>ApiCall</c>/<c>DbWrite</c>, cached-call lifecycle rows,
|
||||||
/// rows from one script run can be correlated together.
|
/// <c>NotifySend</c>) is stamped into <c>AuditEvent.ExecutionId</c> with
|
||||||
|
/// this value so all the rows from one script run can be correlated
|
||||||
|
/// together — independently of the per-operation
|
||||||
|
/// <c>AuditEvent.CorrelationId</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly Guid _auditCorrelationId;
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
/// <param name="auditCorrelationId">
|
/// <param name="executionId">
|
||||||
/// Audit Log #23: the execution-wide audit correlation id. When omitted
|
/// Audit Log #23: the per-execution id for this script run. When omitted
|
||||||
/// (tag-change / timer-triggered executions) a fresh id is generated; an
|
/// (tag-change / timer-triggered executions) a fresh id is generated; an
|
||||||
/// inbound caller may supply one to tie the execution to an upstream
|
/// inbound caller may supply one to tie the execution to an upstream
|
||||||
/// request. Stamped on the sync <c>ApiCall</c>/<c>DbWrite</c> audit rows
|
/// request. Stamped into <c>AuditEvent.ExecutionId</c> on every
|
||||||
/// this execution emits.
|
/// trust-boundary audit row this execution emits.
|
||||||
/// </param>
|
/// </param>
|
||||||
public ScriptRuntimeContext(
|
public ScriptRuntimeContext(
|
||||||
IActorRef instanceActor,
|
IActorRef instanceActor,
|
||||||
@@ -138,7 +141,7 @@ public class ScriptRuntimeContext
|
|||||||
IAuditWriter? auditWriter = null,
|
IAuditWriter? auditWriter = null,
|
||||||
IOperationTrackingStore? operationTrackingStore = null,
|
IOperationTrackingStore? operationTrackingStore = null,
|
||||||
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
ICachedCallTelemetryForwarder? cachedForwarder = null,
|
||||||
Guid? auditCorrelationId = null)
|
Guid? executionId = null)
|
||||||
{
|
{
|
||||||
_instanceActor = instanceActor;
|
_instanceActor = instanceActor;
|
||||||
_self = self;
|
_self = self;
|
||||||
@@ -157,7 +160,7 @@ public class ScriptRuntimeContext
|
|||||||
_auditWriter = auditWriter;
|
_auditWriter = auditWriter;
|
||||||
_operationTrackingStore = operationTrackingStore;
|
_operationTrackingStore = operationTrackingStore;
|
||||||
_cachedForwarder = cachedForwarder;
|
_cachedForwarder = cachedForwarder;
|
||||||
_auditCorrelationId = auditCorrelationId ?? Guid.NewGuid();
|
_executionId = executionId ?? Guid.NewGuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -258,7 +261,7 @@ public class ScriptRuntimeContext
|
|||||||
/// ExternalSystem.CachedCall("systemName", "methodName", params)
|
/// ExternalSystem.CachedCall("systemName", "methodName", params)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ExternalSystemHelper ExternalSystem => new(
|
public ExternalSystemHelper ExternalSystem => new(
|
||||||
_externalSystemClient, _instanceName, _logger, _auditCorrelationId, _auditWriter, _siteId, _sourceScript,
|
_externalSystemClient, _instanceName, _logger, _executionId, _auditWriter, _siteId, _sourceScript,
|
||||||
// Audit Log #23 (M3 Bundle E — Task E3): emit CachedSubmit telemetry
|
// Audit Log #23 (M3 Bundle E — Task E3): emit CachedSubmit telemetry
|
||||||
// on every ExternalSystem.CachedCall enqueue.
|
// on every ExternalSystem.CachedCall enqueue.
|
||||||
_cachedForwarder);
|
_cachedForwarder);
|
||||||
@@ -272,7 +275,7 @@ public class ScriptRuntimeContext
|
|||||||
_databaseGateway,
|
_databaseGateway,
|
||||||
_instanceName,
|
_instanceName,
|
||||||
_logger,
|
_logger,
|
||||||
_auditCorrelationId,
|
_executionId,
|
||||||
// Audit Log #23 (M4 Bundle A): wire the IAuditWriter so
|
// Audit Log #23 (M4 Bundle A): wire the IAuditWriter so
|
||||||
// Database.Connection(name) returns an auditing decorator that
|
// Database.Connection(name) returns an auditing decorator that
|
||||||
// emits one DbOutbound/DbWrite row per script-initiated
|
// emits one DbOutbound/DbWrite row per script-initiated
|
||||||
@@ -299,7 +302,7 @@ public class ScriptRuntimeContext
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public NotifyHelper Notify => new(
|
public NotifyHelper Notify => new(
|
||||||
_storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _sourceScript, _askTimeout, _logger,
|
_storeAndForward, _siteCommunicationActor, _siteId, _instanceName, _sourceScript, _askTimeout, _logger,
|
||||||
_auditWriter);
|
_executionId, _auditWriter);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23 (M3): site-local tracking-status API for cached operations.
|
/// Audit Log #23 (M3): site-local tracking-status API for cached operations.
|
||||||
@@ -380,7 +383,7 @@ public class ScriptRuntimeContext
|
|||||||
private readonly IExternalSystemClient? _client;
|
private readonly IExternalSystemClient? _client;
|
||||||
private readonly string _instanceName;
|
private readonly string _instanceName;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Guid _auditCorrelationId;
|
private readonly Guid _executionId;
|
||||||
private readonly IAuditWriter? _auditWriter;
|
private readonly IAuditWriter? _auditWriter;
|
||||||
private readonly string _siteId;
|
private readonly string _siteId;
|
||||||
private readonly string? _sourceScript;
|
private readonly string? _sourceScript;
|
||||||
@@ -390,7 +393,7 @@ public class ScriptRuntimeContext
|
|||||||
// (via InternalsVisibleTo). Production sites resolve the helper through
|
// (via InternalsVisibleTo). Production sites resolve the helper through
|
||||||
// ScriptRuntimeContext.ExternalSystem.
|
// ScriptRuntimeContext.ExternalSystem.
|
||||||
//
|
//
|
||||||
// Parameter ordering: auditCorrelationId sits immediately after the
|
// Parameter ordering: executionId sits immediately after the
|
||||||
// ILogger across all four audit-threaded ctors (ExternalSystemHelper,
|
// ILogger across all four audit-threaded ctors (ExternalSystemHelper,
|
||||||
// DatabaseHelper, AuditingDbConnection, AuditingDbCommand) — a required
|
// DatabaseHelper, AuditingDbConnection, AuditingDbCommand) — a required
|
||||||
// Guid cannot follow the optional provenance params without a
|
// Guid cannot follow the optional provenance params without a
|
||||||
@@ -400,7 +403,7 @@ public class ScriptRuntimeContext
|
|||||||
IExternalSystemClient? client,
|
IExternalSystemClient? client,
|
||||||
string instanceName,
|
string instanceName,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Guid auditCorrelationId,
|
Guid executionId,
|
||||||
IAuditWriter? auditWriter = null,
|
IAuditWriter? auditWriter = null,
|
||||||
string siteId = "",
|
string siteId = "",
|
||||||
string? sourceScript = null,
|
string? sourceScript = null,
|
||||||
@@ -409,7 +412,7 @@ public class ScriptRuntimeContext
|
|||||||
_client = client;
|
_client = client;
|
||||||
_instanceName = instanceName;
|
_instanceName = instanceName;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_auditCorrelationId = auditCorrelationId;
|
_executionId = executionId;
|
||||||
_auditWriter = auditWriter;
|
_auditWriter = auditWriter;
|
||||||
_siteId = siteId;
|
_siteId = siteId;
|
||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
@@ -567,7 +570,11 @@ public class ScriptRuntimeContext
|
|||||||
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
||||||
Channel = AuditChannel.ApiOutbound,
|
Channel = AuditChannel.ApiOutbound,
|
||||||
Kind = AuditKind.CachedSubmit,
|
Kind = AuditKind.CachedSubmit,
|
||||||
|
// CorrelationId stays the per-operation lifecycle id
|
||||||
|
// (TrackedOperationId); ExecutionId carries the
|
||||||
|
// per-execution id shared across this script run.
|
||||||
CorrelationId = trackedId.Value,
|
CorrelationId = trackedId.Value,
|
||||||
|
ExecutionId = _executionId,
|
||||||
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||||
SourceInstanceId = _instanceName,
|
SourceInstanceId = _instanceName,
|
||||||
SourceScript = _sourceScript,
|
SourceScript = _sourceScript,
|
||||||
@@ -677,7 +684,10 @@ public class ScriptRuntimeContext
|
|||||||
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
||||||
Channel = AuditChannel.ApiOutbound,
|
Channel = AuditChannel.ApiOutbound,
|
||||||
Kind = AuditKind.ApiCallCached,
|
Kind = AuditKind.ApiCallCached,
|
||||||
|
// CorrelationId = per-operation lifecycle id;
|
||||||
|
// ExecutionId = per-execution id for this script run.
|
||||||
CorrelationId = trackedId.Value,
|
CorrelationId = trackedId.Value,
|
||||||
|
ExecutionId = _executionId,
|
||||||
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||||
SourceInstanceId = _instanceName,
|
SourceInstanceId = _instanceName,
|
||||||
SourceScript = _sourceScript,
|
SourceScript = _sourceScript,
|
||||||
@@ -738,7 +748,10 @@ public class ScriptRuntimeContext
|
|||||||
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
||||||
Channel = AuditChannel.ApiOutbound,
|
Channel = AuditChannel.ApiOutbound,
|
||||||
Kind = AuditKind.CachedResolve,
|
Kind = AuditKind.CachedResolve,
|
||||||
|
// CorrelationId = per-operation lifecycle id;
|
||||||
|
// ExecutionId = per-execution id for this script run.
|
||||||
CorrelationId = trackedId.Value,
|
CorrelationId = trackedId.Value,
|
||||||
|
ExecutionId = _executionId,
|
||||||
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||||
SourceInstanceId = _instanceName,
|
SourceInstanceId = _instanceName,
|
||||||
SourceScript = _sourceScript,
|
SourceScript = _sourceScript,
|
||||||
@@ -910,9 +923,12 @@ public class ScriptRuntimeContext
|
|||||||
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
||||||
Channel = AuditChannel.ApiOutbound,
|
Channel = AuditChannel.ApiOutbound,
|
||||||
Kind = AuditKind.ApiCall,
|
Kind = AuditKind.ApiCall,
|
||||||
// Audit Log #23: the execution-wide correlation id, so all the
|
// Audit Log #23: a sync one-shot call has no operation
|
||||||
// sync ApiCall/DbWrite rows from one script run share an id.
|
// lifecycle, so CorrelationId is null. ExecutionId carries the
|
||||||
CorrelationId = _auditCorrelationId,
|
// per-execution id so all the sync ApiCall/DbWrite rows from
|
||||||
|
// one script run can be correlated together.
|
||||||
|
CorrelationId = null,
|
||||||
|
ExecutionId = _executionId,
|
||||||
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||||
SourceInstanceId = _instanceName,
|
SourceInstanceId = _instanceName,
|
||||||
SourceScript = _sourceScript,
|
SourceScript = _sourceScript,
|
||||||
@@ -979,7 +995,7 @@ public class ScriptRuntimeContext
|
|||||||
private readonly IDatabaseGateway? _gateway;
|
private readonly IDatabaseGateway? _gateway;
|
||||||
private readonly string _instanceName;
|
private readonly string _instanceName;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly Guid _auditCorrelationId;
|
private readonly Guid _executionId;
|
||||||
private readonly string _siteId;
|
private readonly string _siteId;
|
||||||
private readonly string? _sourceScript;
|
private readonly string? _sourceScript;
|
||||||
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
private readonly ICachedCallTelemetryForwarder? _cachedForwarder;
|
||||||
@@ -996,7 +1012,7 @@ public class ScriptRuntimeContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IAuditWriter? _auditWriter;
|
private readonly IAuditWriter? _auditWriter;
|
||||||
|
|
||||||
// Parameter ordering: auditCorrelationId sits immediately after the
|
// Parameter ordering: executionId sits immediately after the
|
||||||
// ILogger — see the note on ExternalSystemHelper's ctor for why the
|
// ILogger — see the note on ExternalSystemHelper's ctor for why the
|
||||||
// post-logger slot is the one consistent position across all four
|
// post-logger slot is the one consistent position across all four
|
||||||
// audit-threaded ctors.
|
// audit-threaded ctors.
|
||||||
@@ -1004,7 +1020,7 @@ public class ScriptRuntimeContext
|
|||||||
IDatabaseGateway? gateway,
|
IDatabaseGateway? gateway,
|
||||||
string instanceName,
|
string instanceName,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
Guid auditCorrelationId,
|
Guid executionId,
|
||||||
IAuditWriter? auditWriter = null,
|
IAuditWriter? auditWriter = null,
|
||||||
string siteId = "",
|
string siteId = "",
|
||||||
string? sourceScript = null,
|
string? sourceScript = null,
|
||||||
@@ -1013,7 +1029,7 @@ public class ScriptRuntimeContext
|
|||||||
_gateway = gateway;
|
_gateway = gateway;
|
||||||
_instanceName = instanceName;
|
_instanceName = instanceName;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_auditCorrelationId = auditCorrelationId;
|
_executionId = executionId;
|
||||||
_auditWriter = auditWriter;
|
_auditWriter = auditWriter;
|
||||||
_siteId = siteId;
|
_siteId = siteId;
|
||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
@@ -1049,7 +1065,7 @@ public class ScriptRuntimeContext
|
|||||||
instanceName: _instanceName,
|
instanceName: _instanceName,
|
||||||
sourceScript: _sourceScript,
|
sourceScript: _sourceScript,
|
||||||
logger: _logger,
|
logger: _logger,
|
||||||
auditCorrelationId: _auditCorrelationId);
|
executionId: _executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1116,7 +1132,10 @@ public class ScriptRuntimeContext
|
|||||||
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
||||||
Channel = AuditChannel.DbOutbound,
|
Channel = AuditChannel.DbOutbound,
|
||||||
Kind = AuditKind.CachedSubmit,
|
Kind = AuditKind.CachedSubmit,
|
||||||
|
// CorrelationId = per-operation lifecycle id
|
||||||
|
// (TrackedOperationId); ExecutionId = per-execution id.
|
||||||
CorrelationId = trackedId.Value,
|
CorrelationId = trackedId.Value,
|
||||||
|
ExecutionId = _executionId,
|
||||||
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||||
SourceInstanceId = _instanceName,
|
SourceInstanceId = _instanceName,
|
||||||
SourceScript = _sourceScript,
|
SourceScript = _sourceScript,
|
||||||
@@ -1178,6 +1197,12 @@ public class ScriptRuntimeContext
|
|||||||
private readonly TimeSpan _askTimeout;
|
private readonly TimeSpan _askTimeout;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23: the per-execution id for this script run, stamped
|
||||||
|
/// into <c>AuditEvent.ExecutionId</c> on the <c>NotifySend</c> row.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
||||||
/// <c>Notification</c>/<c>NotifySend</c> row produced when the script
|
/// <c>Notification</c>/<c>NotifySend</c> row produced when the script
|
||||||
@@ -1188,6 +1213,8 @@ public class ScriptRuntimeContext
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly IAuditWriter? _auditWriter;
|
private readonly IAuditWriter? _auditWriter;
|
||||||
|
|
||||||
|
// Parameter ordering: executionId sits immediately after the ILogger,
|
||||||
|
// consistent with the other audit-threaded ctors.
|
||||||
internal NotifyHelper(
|
internal NotifyHelper(
|
||||||
StoreAndForwardService? storeAndForward,
|
StoreAndForwardService? storeAndForward,
|
||||||
ICanTell? siteCommunicationActor,
|
ICanTell? siteCommunicationActor,
|
||||||
@@ -1196,6 +1223,7 @@ public class ScriptRuntimeContext
|
|||||||
string? sourceScript,
|
string? sourceScript,
|
||||||
TimeSpan askTimeout,
|
TimeSpan askTimeout,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
Guid executionId,
|
||||||
IAuditWriter? auditWriter = null)
|
IAuditWriter? auditWriter = null)
|
||||||
{
|
{
|
||||||
_storeAndForward = storeAndForward;
|
_storeAndForward = storeAndForward;
|
||||||
@@ -1205,6 +1233,7 @@ public class ScriptRuntimeContext
|
|||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
_askTimeout = askTimeout;
|
_askTimeout = askTimeout;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_executionId = executionId;
|
||||||
_auditWriter = auditWriter;
|
_auditWriter = auditWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,6 +1244,9 @@ public class ScriptRuntimeContext
|
|||||||
{
|
{
|
||||||
return new NotifyTarget(
|
return new NotifyTarget(
|
||||||
listName, _storeAndForward, _siteId, _instanceName, _sourceScript, _logger,
|
listName, _storeAndForward, _siteId, _instanceName, _sourceScript, _logger,
|
||||||
|
// Audit Log #23: the per-execution id stamped into the
|
||||||
|
// NotifySend row's ExecutionId column.
|
||||||
|
_executionId,
|
||||||
// Audit Log #23 (M4 Bundle C): forward the writer so Send()
|
// Audit Log #23 (M4 Bundle C): forward the writer so Send()
|
||||||
// can emit one NotifySend(Submitted) row per accepted submission.
|
// can emit one NotifySend(Submitted) row per accepted submission.
|
||||||
_auditWriter);
|
_auditWriter);
|
||||||
@@ -1292,6 +1324,12 @@ public class ScriptRuntimeContext
|
|||||||
private readonly string? _sourceScript;
|
private readonly string? _sourceScript;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23: the per-execution id for this script run, stamped
|
||||||
|
/// into <c>AuditEvent.ExecutionId</c> on the <c>NotifySend</c> row.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid _executionId;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
/// Audit Log #23 (M4 Bundle C): best-effort emitter for the
|
||||||
/// <c>Notification</c>/<c>NotifySend</c> row written immediately after
|
/// <c>Notification</c>/<c>NotifySend</c> row written immediately after
|
||||||
@@ -1307,6 +1345,7 @@ public class ScriptRuntimeContext
|
|||||||
string instanceName,
|
string instanceName,
|
||||||
string? sourceScript,
|
string? sourceScript,
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
|
Guid executionId,
|
||||||
IAuditWriter? auditWriter = null)
|
IAuditWriter? auditWriter = null)
|
||||||
{
|
{
|
||||||
_listName = listName;
|
_listName = listName;
|
||||||
@@ -1315,6 +1354,7 @@ public class ScriptRuntimeContext
|
|||||||
_instanceName = instanceName;
|
_instanceName = instanceName;
|
||||||
_sourceScript = sourceScript;
|
_sourceScript = sourceScript;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_executionId = executionId;
|
||||||
_auditWriter = auditWriter;
|
_auditWriter = auditWriter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,7 +1471,10 @@ public class ScriptRuntimeContext
|
|||||||
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
OccurredAtUtc = DateTime.SpecifyKind(occurredAtUtc, DateTimeKind.Utc),
|
||||||
Channel = AuditChannel.Notification,
|
Channel = AuditChannel.Notification,
|
||||||
Kind = AuditKind.NotifySend,
|
Kind = AuditKind.NotifySend,
|
||||||
|
// CorrelationId is the NotificationId-derived per-operation
|
||||||
|
// lifecycle id; ExecutionId carries the per-execution id.
|
||||||
CorrelationId = correlationId,
|
CorrelationId = correlationId,
|
||||||
|
ExecutionId = _executionId,
|
||||||
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
SourceSiteId = string.IsNullOrEmpty(_siteId) ? null : _siteId,
|
||||||
SourceInstanceId = _instanceName,
|
SourceInstanceId = _instanceName,
|
||||||
SourceScript = _sourceScript,
|
SourceScript = _sourceScript,
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ public class DatabaseCachedWriteEmissionTests
|
|||||||
private const string InstanceName = "Plant.Pump42";
|
private const string InstanceName = "Plant.Pump42";
|
||||||
private const string SourceScript = "ScriptActor:WriteAudit";
|
private const string SourceScript = "ScriptActor:WriteAudit";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23: a fixed per-execution id so the cached-row tests can
|
||||||
|
/// assert <see cref="AuditEvent.ExecutionId"/> against a known value.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Guid TestExecutionId = Guid.NewGuid();
|
||||||
|
|
||||||
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
|
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
|
||||||
IDatabaseGateway gateway,
|
IDatabaseGateway gateway,
|
||||||
ICachedCallTelemetryForwarder? forwarder)
|
ICachedCallTelemetryForwarder? forwarder)
|
||||||
@@ -47,9 +53,10 @@ public class DatabaseCachedWriteEmissionTests
|
|||||||
gateway,
|
gateway,
|
||||||
InstanceName,
|
InstanceName,
|
||||||
NullLogger.Instance,
|
NullLogger.Instance,
|
||||||
// Audit Log #23: execution-wide correlation id. Cached rows keep
|
// Audit Log #23: the per-execution id stamped into ExecutionId on
|
||||||
// CorrelationId = TrackedOperationId, so any value works here.
|
// every script-side row. Cached rows keep CorrelationId =
|
||||||
Guid.NewGuid(),
|
// TrackedOperationId (the per-operation lifecycle id).
|
||||||
|
TestExecutionId,
|
||||||
siteId: SiteId,
|
siteId: SiteId,
|
||||||
sourceScript: SourceScript,
|
sourceScript: SourceScript,
|
||||||
cachedForwarder: forwarder);
|
cachedForwarder: forwarder);
|
||||||
@@ -79,7 +86,10 @@ public class DatabaseCachedWriteEmissionTests
|
|||||||
Assert.Equal(AuditKind.CachedSubmit, packet.Audit.Kind);
|
Assert.Equal(AuditKind.CachedSubmit, packet.Audit.Kind);
|
||||||
Assert.Equal(AuditStatus.Submitted, packet.Audit.Status);
|
Assert.Equal(AuditStatus.Submitted, packet.Audit.Status);
|
||||||
Assert.Equal("myDb", packet.Audit.Target);
|
Assert.Equal("myDb", packet.Audit.Target);
|
||||||
|
// CorrelationId is the per-operation lifecycle id (TrackedOperationId);
|
||||||
|
// ExecutionId is the per-execution id from the runtime context.
|
||||||
Assert.Equal(trackedId.Value, packet.Audit.CorrelationId);
|
Assert.Equal(trackedId.Value, packet.Audit.CorrelationId);
|
||||||
|
Assert.Equal(TestExecutionId, packet.Audit.ExecutionId);
|
||||||
|
|
||||||
Assert.Equal(trackedId, packet.Operational.TrackedOperationId);
|
Assert.Equal(trackedId, packet.Operational.TrackedOperationId);
|
||||||
Assert.Equal("DbOutbound", packet.Operational.Channel);
|
Assert.Equal("DbOutbound", packet.Operational.Channel);
|
||||||
|
|||||||
@@ -49,27 +49,27 @@ public class DatabaseSyncEmissionTests
|
|||||||
private const string ConnectionName = "machineData";
|
private const string ConnectionName = "machineData";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23: a fixed execution-wide correlation id used by the
|
/// Audit Log #23: a fixed per-execution id used by the default
|
||||||
/// default <see cref="CreateHelper(IDatabaseGateway, IAuditWriter?)"/>
|
/// <see cref="CreateHelper(IDatabaseGateway, IAuditWriter?)"/>
|
||||||
/// overload so assertions can compare against a known value.
|
/// overload so assertions can compare against a known value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Guid TestCorrelationId = Guid.NewGuid();
|
private static readonly Guid TestExecutionId = Guid.NewGuid();
|
||||||
|
|
||||||
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
|
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
|
||||||
IDatabaseGateway gateway,
|
IDatabaseGateway gateway,
|
||||||
IAuditWriter? auditWriter)
|
IAuditWriter? auditWriter)
|
||||||
=> CreateHelper(gateway, auditWriter, TestCorrelationId);
|
=> CreateHelper(gateway, auditWriter, TestExecutionId);
|
||||||
|
|
||||||
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
|
private static ScriptRuntimeContext.DatabaseHelper CreateHelper(
|
||||||
IDatabaseGateway gateway,
|
IDatabaseGateway gateway,
|
||||||
IAuditWriter? auditWriter,
|
IAuditWriter? auditWriter,
|
||||||
Guid correlationId)
|
Guid executionId)
|
||||||
{
|
{
|
||||||
return new ScriptRuntimeContext.DatabaseHelper(
|
return new ScriptRuntimeContext.DatabaseHelper(
|
||||||
gateway,
|
gateway,
|
||||||
InstanceName,
|
InstanceName,
|
||||||
NullLogger.Instance,
|
NullLogger.Instance,
|
||||||
correlationId,
|
executionId,
|
||||||
auditWriter: auditWriter,
|
auditWriter: auditWriter,
|
||||||
siteId: SiteId,
|
siteId: SiteId,
|
||||||
sourceScript: SourceScript,
|
sourceScript: SourceScript,
|
||||||
@@ -282,14 +282,16 @@ public class DatabaseSyncEmissionTests
|
|||||||
Assert.Equal(SourceScript, evt.SourceScript);
|
Assert.Equal(SourceScript, evt.SourceScript);
|
||||||
// Outbound channel: Actor carries the calling script identity.
|
// Outbound channel: Actor carries the calling script identity.
|
||||||
Assert.Equal(SourceScript, evt.Actor);
|
Assert.Equal(SourceScript, evt.Actor);
|
||||||
// Audit Log #23: the sync DbWrite row now carries the execution-wide
|
// Audit Log #23: the sync DbWrite row carries the per-execution id the
|
||||||
// correlation id the helper was constructed with.
|
// helper was constructed with in ExecutionId. CorrelationId is null —
|
||||||
Assert.Equal(TestCorrelationId, evt.CorrelationId);
|
// a sync one-shot call has no operation lifecycle.
|
||||||
|
Assert.Equal(TestExecutionId, evt.ExecutionId);
|
||||||
|
Assert.Null(evt.CorrelationId);
|
||||||
Assert.NotEqual(Guid.Empty, evt.EventId);
|
Assert.NotEqual(Guid.Empty, evt.EventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SyncDbWrite_StampsExecutionCorrelationId()
|
public async Task SyncDbWrite_StampsExecutionId_AndNullCorrelationId()
|
||||||
{
|
{
|
||||||
using var keepAlive = new SqliteConnection("Data Source=kc;Mode=Memory;Cache=Shared");
|
using var keepAlive = new SqliteConnection("Data Source=kc;Mode=Memory;Cache=Shared");
|
||||||
var inner = NewInMemoryDb(out var _);
|
var inner = NewInMemoryDb(out var _);
|
||||||
@@ -298,16 +300,18 @@ public class DatabaseSyncEmissionTests
|
|||||||
.Setup(g => g.GetConnectionAsync(ConnectionName, It.IsAny<CancellationToken>()))
|
.Setup(g => g.GetConnectionAsync(ConnectionName, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(inner);
|
.ReturnsAsync(inner);
|
||||||
var writer = new CapturingAuditWriter();
|
var writer = new CapturingAuditWriter();
|
||||||
var correlationId = Guid.NewGuid();
|
var executionId = Guid.NewGuid();
|
||||||
|
|
||||||
var helper = CreateHelper(gateway.Object, writer, correlationId);
|
var helper = CreateHelper(gateway.Object, writer, executionId);
|
||||||
await using var conn = await helper.Connection(ConnectionName);
|
await using var conn = await helper.Connection(ConnectionName);
|
||||||
await using var cmd = conn.CreateCommand();
|
await using var cmd = conn.CreateCommand();
|
||||||
cmd.CommandText = "INSERT INTO t (id, name) VALUES (7, 'eta')";
|
cmd.CommandText = "INSERT INTO t (id, name) VALUES (7, 'eta')";
|
||||||
await cmd.ExecuteNonQueryAsync();
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
var evt = Assert.Single(writer.Events);
|
var evt = Assert.Single(writer.Events);
|
||||||
Assert.Equal(correlationId, evt.CorrelationId);
|
Assert.Equal(executionId, evt.ExecutionId);
|
||||||
|
// Sync one-shot call: CorrelationId is null (no operation lifecycle).
|
||||||
|
Assert.Null(evt.CorrelationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -16,13 +16,15 @@ namespace ScadaLink.SiteRuntime.Tests.Scripts;
|
|||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// The <c>?? Guid.NewGuid()</c> fallback in the <see cref="ScriptRuntimeContext"/>
|
/// The <c>?? Guid.NewGuid()</c> fallback in the <see cref="ScriptRuntimeContext"/>
|
||||||
/// ctor: when no audit correlation id is supplied (tag-change / timer-triggered
|
/// ctor: when no execution id is supplied (tag-change / timer-triggered
|
||||||
/// executions) a fresh, non-empty id is minted and stamped on the emitted rows.
|
/// executions) a fresh, non-empty id is minted and stamped on the emitted rows.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// <item><description>
|
/// <item><description>
|
||||||
/// The execution-wide contract: an <c>ExternalSystem.Call</c> and a sync
|
/// The execution-wide contract: an <c>ExternalSystem.Call</c> and a sync
|
||||||
/// <c>Database</c> write performed through ONE context share a single
|
/// <c>Database</c> write performed through ONE context share a single
|
||||||
/// <see cref="AuditEvent.CorrelationId"/>.
|
/// <see cref="AuditEvent.ExecutionId"/>. The per-operation
|
||||||
|
/// <see cref="AuditEvent.CorrelationId"/> stays null for these sync one-shot
|
||||||
|
/// calls — a sync call has no operation lifecycle.
|
||||||
/// </description></item>
|
/// </description></item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -53,14 +55,14 @@ public class ExecutionCorrelationContextTests
|
|||||||
/// system client, database gateway and audit writer the cross-helper test
|
/// system client, database gateway and audit writer the cross-helper test
|
||||||
/// needs. The actor refs are <see cref="ActorRefs.Nobody"/> — the
|
/// needs. The actor refs are <see cref="ActorRefs.Nobody"/> — the
|
||||||
/// integration helpers (ExternalSystem / Database) never touch them — and
|
/// integration helpers (ExternalSystem / Database) never touch them — and
|
||||||
/// <paramref name="auditCorrelationId"/> defaults to null so the ctor's
|
/// <paramref name="executionId"/> defaults to null so the ctor's
|
||||||
/// <c>?? Guid.NewGuid()</c> fallback is exercised unless a test supplies one.
|
/// <c>?? Guid.NewGuid()</c> fallback is exercised unless a test supplies one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static ScriptRuntimeContext CreateContext(
|
private static ScriptRuntimeContext CreateContext(
|
||||||
IExternalSystemClient? externalSystemClient,
|
IExternalSystemClient? externalSystemClient,
|
||||||
IDatabaseGateway? databaseGateway,
|
IDatabaseGateway? databaseGateway,
|
||||||
IAuditWriter? auditWriter,
|
IAuditWriter? auditWriter,
|
||||||
Guid? auditCorrelationId = null)
|
Guid? executionId = null)
|
||||||
{
|
{
|
||||||
var compilationService = new ScriptCompilationService(
|
var compilationService = new ScriptCompilationService(
|
||||||
NullLogger<ScriptCompilationService>.Instance);
|
NullLogger<ScriptCompilationService>.Instance);
|
||||||
@@ -85,7 +87,7 @@ public class ExecutionCorrelationContextTests
|
|||||||
auditWriter: auditWriter,
|
auditWriter: auditWriter,
|
||||||
operationTrackingStore: null,
|
operationTrackingStore: null,
|
||||||
cachedForwarder: null,
|
cachedForwarder: null,
|
||||||
auditCorrelationId: auditCorrelationId);
|
executionId: executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -113,9 +115,9 @@ public class ExecutionCorrelationContextTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task NoCorrelationIdSupplied_SyncCall_StampsFreshNonEmptyCorrelationId()
|
public async Task NoExecutionIdSupplied_SyncCall_StampsFreshNonEmptyExecutionId()
|
||||||
{
|
{
|
||||||
// No auditCorrelationId argument — the ScriptRuntimeContext ctor's
|
// No executionId argument — the ScriptRuntimeContext ctor's
|
||||||
// `?? Guid.NewGuid()` fallback must mint one (this is the unsupplied-id
|
// `?? Guid.NewGuid()` fallback must mint one (this is the unsupplied-id
|
||||||
// branch every other audit test bypasses by passing an explicit id).
|
// branch every other audit test bypasses by passing an explicit id).
|
||||||
var client = new Mock<IExternalSystemClient>();
|
var client = new Mock<IExternalSystemClient>();
|
||||||
@@ -128,17 +130,19 @@ public class ExecutionCorrelationContextTests
|
|||||||
await context.ExternalSystem.Call("ERP", "GetOrder");
|
await context.ExternalSystem.Call("ERP", "GetOrder");
|
||||||
|
|
||||||
var evt = Assert.Single(writer.Events);
|
var evt = Assert.Single(writer.Events);
|
||||||
Assert.NotNull(evt.CorrelationId);
|
Assert.NotNull(evt.ExecutionId);
|
||||||
Assert.NotEqual(Guid.Empty, evt.CorrelationId!.Value);
|
Assert.NotEqual(Guid.Empty, evt.ExecutionId!.Value);
|
||||||
|
// A sync one-shot call has no operation lifecycle — CorrelationId is null.
|
||||||
|
Assert.Null(evt.CorrelationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SameContext_ApiCallAndDbWrite_ShareTheSameCorrelationId()
|
public async Task SameContext_ApiCallAndDbWrite_ShareTheSameExecutionId()
|
||||||
{
|
{
|
||||||
// The execution-wide contract: an ExternalSystem.Call AND a sync
|
// The execution-wide contract: an ExternalSystem.Call AND a sync
|
||||||
// Database write performed through ONE ScriptRuntimeContext must both
|
// Database write performed through ONE ScriptRuntimeContext must both
|
||||||
// carry the same execution correlation id, so an audit reader can tie
|
// carry the same ExecutionId, so an audit reader can tie every
|
||||||
// every trust-boundary action from one script run together.
|
// trust-boundary action from one script run together.
|
||||||
using var keepAlive = new SqliteConnection("Data Source=ecc;Mode=Memory;Cache=Shared");
|
using var keepAlive = new SqliteConnection("Data Source=ecc;Mode=Memory;Cache=Shared");
|
||||||
var innerDb = NewInMemoryDb(out var _);
|
var innerDb = NewInMemoryDb(out var _);
|
||||||
|
|
||||||
@@ -170,10 +174,13 @@ public class ExecutionCorrelationContextTests
|
|||||||
var apiRow = Assert.Single(writer.Events, e => e.Channel == AuditChannel.ApiOutbound);
|
var apiRow = Assert.Single(writer.Events, e => e.Channel == AuditChannel.ApiOutbound);
|
||||||
var dbRow = Assert.Single(writer.Events, e => e.Channel == AuditChannel.DbOutbound);
|
var dbRow = Assert.Single(writer.Events, e => e.Channel == AuditChannel.DbOutbound);
|
||||||
|
|
||||||
Assert.NotNull(apiRow.CorrelationId);
|
Assert.NotNull(apiRow.ExecutionId);
|
||||||
Assert.NotEqual(Guid.Empty, apiRow.CorrelationId!.Value);
|
Assert.NotEqual(Guid.Empty, apiRow.ExecutionId!.Value);
|
||||||
// The ApiCall row and the DbWrite row, emitted by two different helpers
|
// The ApiCall row and the DbWrite row, emitted by two different helpers
|
||||||
// resolved off one context, carry the identical execution correlation id.
|
// resolved off one context, carry the identical ExecutionId.
|
||||||
Assert.Equal(apiRow.CorrelationId, dbRow.CorrelationId);
|
Assert.Equal(apiRow.ExecutionId, dbRow.ExecutionId);
|
||||||
|
// Both are sync one-shot calls — neither carries a CorrelationId.
|
||||||
|
Assert.Null(apiRow.CorrelationId);
|
||||||
|
Assert.Null(dbRow.CorrelationId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ public class ExternalSystemCachedCallEmissionTests
|
|||||||
private const string InstanceName = "Plant.Pump42";
|
private const string InstanceName = "Plant.Pump42";
|
||||||
private const string SourceScript = "ScriptActor:CheckPressure";
|
private const string SourceScript = "ScriptActor:CheckPressure";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23: a fixed per-execution id so the cached-row tests can
|
||||||
|
/// assert <see cref="AuditEvent.ExecutionId"/> against a known value.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Guid TestExecutionId = Guid.NewGuid();
|
||||||
|
|
||||||
private static ScriptRuntimeContext.ExternalSystemHelper CreateHelper(
|
private static ScriptRuntimeContext.ExternalSystemHelper CreateHelper(
|
||||||
IExternalSystemClient client,
|
IExternalSystemClient client,
|
||||||
ICachedCallTelemetryForwarder? forwarder)
|
ICachedCallTelemetryForwarder? forwarder)
|
||||||
@@ -49,9 +55,10 @@ public class ExternalSystemCachedCallEmissionTests
|
|||||||
client,
|
client,
|
||||||
InstanceName,
|
InstanceName,
|
||||||
NullLogger.Instance,
|
NullLogger.Instance,
|
||||||
// Audit Log #23: execution-wide correlation id. Cached rows keep
|
// Audit Log #23: the per-execution id stamped into ExecutionId on
|
||||||
// CorrelationId = TrackedOperationId, so any value works here.
|
// every script-side row. Cached rows keep CorrelationId =
|
||||||
Guid.NewGuid(),
|
// TrackedOperationId (the per-operation lifecycle id).
|
||||||
|
TestExecutionId,
|
||||||
auditWriter: null,
|
auditWriter: null,
|
||||||
siteId: SiteId,
|
siteId: SiteId,
|
||||||
sourceScript: SourceScript,
|
sourceScript: SourceScript,
|
||||||
@@ -83,7 +90,10 @@ public class ExternalSystemCachedCallEmissionTests
|
|||||||
Assert.Equal(AuditKind.CachedSubmit, packet.Audit.Kind);
|
Assert.Equal(AuditKind.CachedSubmit, packet.Audit.Kind);
|
||||||
Assert.Equal(AuditStatus.Submitted, packet.Audit.Status);
|
Assert.Equal(AuditStatus.Submitted, packet.Audit.Status);
|
||||||
Assert.Equal("ERP.GetOrder", packet.Audit.Target);
|
Assert.Equal("ERP.GetOrder", packet.Audit.Target);
|
||||||
|
// CorrelationId is the per-operation lifecycle id (TrackedOperationId);
|
||||||
|
// ExecutionId is the per-execution id from the runtime context.
|
||||||
Assert.Equal(trackedId.Value, packet.Audit.CorrelationId);
|
Assert.Equal(trackedId.Value, packet.Audit.CorrelationId);
|
||||||
|
Assert.Equal(TestExecutionId, packet.Audit.ExecutionId);
|
||||||
Assert.Equal(AuditForwardState.Pending, packet.Audit.ForwardState);
|
Assert.Equal(AuditForwardState.Pending, packet.Audit.ForwardState);
|
||||||
|
|
||||||
// Operational mirror — same id, Submitted, RetryCount 0, not terminal.
|
// Operational mirror — same id, Submitted, RetryCount 0, not terminal.
|
||||||
@@ -298,6 +308,7 @@ public class ExternalSystemCachedCallEmissionTests
|
|||||||
var submit = forwarder.Telemetry[0];
|
var submit = forwarder.Telemetry[0];
|
||||||
Assert.Equal(AuditKind.CachedSubmit, submit.Audit.Kind);
|
Assert.Equal(AuditKind.CachedSubmit, submit.Audit.Kind);
|
||||||
Assert.Equal(AuditStatus.Submitted, submit.Audit.Status);
|
Assert.Equal(AuditStatus.Submitted, submit.Audit.Status);
|
||||||
|
Assert.Equal(TestExecutionId, submit.Audit.ExecutionId);
|
||||||
Assert.Equal(trackedId, submit.Operational.TrackedOperationId);
|
Assert.Equal(trackedId, submit.Operational.TrackedOperationId);
|
||||||
Assert.Null(submit.Operational.TerminalAtUtc);
|
Assert.Null(submit.Operational.TerminalAtUtc);
|
||||||
|
|
||||||
@@ -305,7 +316,10 @@ public class ExternalSystemCachedCallEmissionTests
|
|||||||
Assert.Equal(AuditChannel.ApiOutbound, attempted.Audit.Channel);
|
Assert.Equal(AuditChannel.ApiOutbound, attempted.Audit.Channel);
|
||||||
Assert.Equal(AuditKind.ApiCallCached, attempted.Audit.Kind);
|
Assert.Equal(AuditKind.ApiCallCached, attempted.Audit.Kind);
|
||||||
Assert.Equal(AuditStatus.Attempted, attempted.Audit.Status);
|
Assert.Equal(AuditStatus.Attempted, attempted.Audit.Status);
|
||||||
|
// Cached rows: CorrelationId = TrackedOperationId; ExecutionId is the
|
||||||
|
// per-execution id from the runtime context.
|
||||||
Assert.Equal(trackedId.Value, attempted.Audit.CorrelationId);
|
Assert.Equal(trackedId.Value, attempted.Audit.CorrelationId);
|
||||||
|
Assert.Equal(TestExecutionId, attempted.Audit.ExecutionId);
|
||||||
Assert.Equal("ERP.GetOrder", attempted.Audit.Target);
|
Assert.Equal("ERP.GetOrder", attempted.Audit.Target);
|
||||||
Assert.Equal(trackedId, attempted.Operational.TrackedOperationId);
|
Assert.Equal(trackedId, attempted.Operational.TrackedOperationId);
|
||||||
Assert.Equal("Attempted", attempted.Operational.Status);
|
Assert.Equal("Attempted", attempted.Operational.Status);
|
||||||
@@ -316,6 +330,7 @@ public class ExternalSystemCachedCallEmissionTests
|
|||||||
Assert.Equal(AuditKind.CachedResolve, resolve.Audit.Kind);
|
Assert.Equal(AuditKind.CachedResolve, resolve.Audit.Kind);
|
||||||
Assert.Equal(AuditStatus.Delivered, resolve.Audit.Status);
|
Assert.Equal(AuditStatus.Delivered, resolve.Audit.Status);
|
||||||
Assert.Equal(trackedId.Value, resolve.Audit.CorrelationId);
|
Assert.Equal(trackedId.Value, resolve.Audit.CorrelationId);
|
||||||
|
Assert.Equal(TestExecutionId, resolve.Audit.ExecutionId);
|
||||||
Assert.Equal(trackedId, resolve.Operational.TrackedOperationId);
|
Assert.Equal(trackedId, resolve.Operational.TrackedOperationId);
|
||||||
Assert.Equal("Delivered", resolve.Operational.Status);
|
Assert.Equal("Delivered", resolve.Operational.Status);
|
||||||
// Terminal row carries TerminalAtUtc.
|
// Terminal row carries TerminalAtUtc.
|
||||||
|
|||||||
@@ -46,27 +46,27 @@ public class ExternalSystemCallAuditEmissionTests
|
|||||||
private const string SourceScript = "ScriptActor:CheckPressure";
|
private const string SourceScript = "ScriptActor:CheckPressure";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Audit Log #23: a fixed execution-wide correlation id used by the
|
/// Audit Log #23: a fixed per-execution id used by the default
|
||||||
/// default <see cref="CreateHelper(IExternalSystemClient, IAuditWriter?)"/>
|
/// <see cref="CreateHelper(IExternalSystemClient, IAuditWriter?)"/>
|
||||||
/// overload so assertions can compare against a known value.
|
/// overload so assertions can compare against a known value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Guid TestCorrelationId = Guid.NewGuid();
|
private static readonly Guid TestExecutionId = Guid.NewGuid();
|
||||||
|
|
||||||
private static ScriptRuntimeContext.ExternalSystemHelper CreateHelper(
|
private static ScriptRuntimeContext.ExternalSystemHelper CreateHelper(
|
||||||
IExternalSystemClient client,
|
IExternalSystemClient client,
|
||||||
IAuditWriter? auditWriter)
|
IAuditWriter? auditWriter)
|
||||||
=> CreateHelper(client, auditWriter, TestCorrelationId);
|
=> CreateHelper(client, auditWriter, TestExecutionId);
|
||||||
|
|
||||||
private static ScriptRuntimeContext.ExternalSystemHelper CreateHelper(
|
private static ScriptRuntimeContext.ExternalSystemHelper CreateHelper(
|
||||||
IExternalSystemClient client,
|
IExternalSystemClient client,
|
||||||
IAuditWriter? auditWriter,
|
IAuditWriter? auditWriter,
|
||||||
Guid correlationId)
|
Guid executionId)
|
||||||
{
|
{
|
||||||
return new ScriptRuntimeContext.ExternalSystemHelper(
|
return new ScriptRuntimeContext.ExternalSystemHelper(
|
||||||
client,
|
client,
|
||||||
InstanceName,
|
InstanceName,
|
||||||
NullLogger.Instance,
|
NullLogger.Instance,
|
||||||
correlationId,
|
executionId,
|
||||||
auditWriter,
|
auditWriter,
|
||||||
SiteId,
|
SiteId,
|
||||||
SourceScript);
|
SourceScript);
|
||||||
@@ -225,47 +225,54 @@ public class ExternalSystemCallAuditEmissionTests
|
|||||||
Assert.Equal(SourceScript, evt.SourceScript);
|
Assert.Equal(SourceScript, evt.SourceScript);
|
||||||
// Outbound channel: Actor carries the calling script identity.
|
// Outbound channel: Actor carries the calling script identity.
|
||||||
Assert.Equal(SourceScript, evt.Actor);
|
Assert.Equal(SourceScript, evt.Actor);
|
||||||
// Audit Log #23: the sync ApiCall row now carries the execution-wide
|
// Audit Log #23: the sync ApiCall row carries the per-execution id the
|
||||||
// correlation id the helper was constructed with.
|
// helper was constructed with in ExecutionId. CorrelationId is null —
|
||||||
Assert.Equal(TestCorrelationId, evt.CorrelationId);
|
// a sync one-shot call has no operation lifecycle.
|
||||||
|
Assert.Equal(TestExecutionId, evt.ExecutionId);
|
||||||
|
Assert.Null(evt.CorrelationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Call_SyncApiCall_StampsExecutionCorrelationId()
|
public async Task Call_SyncApiCall_StampsExecutionId_AndNullCorrelationId()
|
||||||
{
|
{
|
||||||
var client = new Mock<IExternalSystemClient>();
|
var client = new Mock<IExternalSystemClient>();
|
||||||
client
|
client
|
||||||
.Setup(c => c.CallAsync("ERP", "GetOrder", It.IsAny<IReadOnlyDictionary<string, object?>?>(), It.IsAny<CancellationToken>()))
|
.Setup(c => c.CallAsync("ERP", "GetOrder", It.IsAny<IReadOnlyDictionary<string, object?>?>(), It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(new ExternalCallResult(true, "{}", null));
|
.ReturnsAsync(new ExternalCallResult(true, "{}", null));
|
||||||
var writer = new CapturingAuditWriter();
|
var writer = new CapturingAuditWriter();
|
||||||
var correlationId = Guid.NewGuid();
|
var executionId = Guid.NewGuid();
|
||||||
|
|
||||||
var helper = CreateHelper(client.Object, writer, correlationId);
|
var helper = CreateHelper(client.Object, writer, executionId);
|
||||||
await helper.Call("ERP", "GetOrder");
|
await helper.Call("ERP", "GetOrder");
|
||||||
|
|
||||||
var evt = Assert.Single(writer.Events);
|
var evt = Assert.Single(writer.Events);
|
||||||
Assert.Equal(correlationId, evt.CorrelationId);
|
Assert.Equal(executionId, evt.ExecutionId);
|
||||||
|
// Sync one-shot call: CorrelationId is null (no operation lifecycle).
|
||||||
|
Assert.Null(evt.CorrelationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Call_TwoCallsOnSameHelper_ShareTheSameCorrelationId()
|
public async Task Call_TwoCallsOnSameHelper_ShareTheSameExecutionId()
|
||||||
{
|
{
|
||||||
var client = new Mock<IExternalSystemClient>();
|
var client = new Mock<IExternalSystemClient>();
|
||||||
client
|
client
|
||||||
.Setup(c => c.CallAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, object?>?>(), It.IsAny<CancellationToken>()))
|
.Setup(c => c.CallAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IReadOnlyDictionary<string, object?>?>(), It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(new ExternalCallResult(true, "{}", null));
|
.ReturnsAsync(new ExternalCallResult(true, "{}", null));
|
||||||
var writer = new CapturingAuditWriter();
|
var writer = new CapturingAuditWriter();
|
||||||
var correlationId = Guid.NewGuid();
|
var executionId = Guid.NewGuid();
|
||||||
|
|
||||||
var helper = CreateHelper(client.Object, writer, correlationId);
|
var helper = CreateHelper(client.Object, writer, executionId);
|
||||||
await helper.Call("ERP", "GetOrder");
|
await helper.Call("ERP", "GetOrder");
|
||||||
await helper.Call("ERP", "GetCustomer");
|
await helper.Call("ERP", "GetCustomer");
|
||||||
|
|
||||||
Assert.Equal(2, writer.Events.Count);
|
Assert.Equal(2, writer.Events.Count);
|
||||||
// Both sync ApiCall rows from one execution carry the same id.
|
// Both sync ApiCall rows from one execution carry the same ExecutionId.
|
||||||
Assert.Equal(correlationId, writer.Events[0].CorrelationId);
|
Assert.Equal(executionId, writer.Events[0].ExecutionId);
|
||||||
Assert.Equal(correlationId, writer.Events[1].CorrelationId);
|
Assert.Equal(executionId, writer.Events[1].ExecutionId);
|
||||||
Assert.Equal(writer.Events[0].CorrelationId, writer.Events[1].CorrelationId);
|
Assert.Equal(writer.Events[0].ExecutionId, writer.Events[1].ExecutionId);
|
||||||
|
// Neither sync call carries a CorrelationId.
|
||||||
|
Assert.Null(writer.Events[0].CorrelationId);
|
||||||
|
Assert.Null(writer.Events[1].CorrelationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -69,7 +69,8 @@ public class NotifyHelperTests : TestKit, IAsyncLifetime, IDisposable
|
|||||||
"Plant.Pump3",
|
"Plant.Pump3",
|
||||||
sourceScript,
|
sourceScript,
|
||||||
TimeSpan.FromSeconds(3),
|
TimeSpan.FromSeconds(3),
|
||||||
NullLogger.Instance);
|
NullLogger.Instance,
|
||||||
|
Guid.NewGuid());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ public class NotifySendAuditEmissionTests : TestKit, IAsyncLifetime, IDisposable
|
|||||||
private const string Subject = "Pump alarm";
|
private const string Subject = "Pump alarm";
|
||||||
private const string Body = "Pump 3 tripped";
|
private const string Body = "Pump 3 tripped";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Audit Log #23: a fixed per-execution id so the NotifySend test can
|
||||||
|
/// assert <see cref="AuditEvent.ExecutionId"/> against a known value.
|
||||||
|
/// </summary>
|
||||||
|
private static readonly Guid TestExecutionId = Guid.NewGuid();
|
||||||
|
|
||||||
private readonly SqliteConnection _keepAlive;
|
private readonly SqliteConnection _keepAlive;
|
||||||
private readonly StoreAndForwardStorage _storage;
|
private readonly StoreAndForwardStorage _storage;
|
||||||
private readonly StoreAndForwardService _saf;
|
private readonly StoreAndForwardService _saf;
|
||||||
@@ -102,6 +108,7 @@ public class NotifySendAuditEmissionTests : TestKit, IAsyncLifetime, IDisposable
|
|||||||
sourceScript,
|
sourceScript,
|
||||||
TimeSpan.FromSeconds(3),
|
TimeSpan.FromSeconds(3),
|
||||||
NullLogger.Instance,
|
NullLogger.Instance,
|
||||||
|
TestExecutionId,
|
||||||
auditWriter);
|
auditWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,12 +221,14 @@ public class NotifySendAuditEmissionTests : TestKit, IAsyncLifetime, IDisposable
|
|||||||
|
|
||||||
// NotificationId is minted as Guid.NewGuid().ToString("N") — the 32-char
|
// NotificationId is minted as Guid.NewGuid().ToString("N") — the 32-char
|
||||||
// hex form, which Guid.TryParse accepts. The audit row's CorrelationId
|
// hex form, which Guid.TryParse accepts. The audit row's CorrelationId
|
||||||
// must round-trip back to the same Guid value.
|
// must round-trip back to the same Guid value (the per-operation
|
||||||
|
// lifecycle id). ExecutionId carries the per-execution id instead.
|
||||||
Assert.True(Guid.TryParse(notificationId, out var expected),
|
Assert.True(Guid.TryParse(notificationId, out var expected),
|
||||||
$"NotificationId '{notificationId}' should be a parseable Guid");
|
$"NotificationId '{notificationId}' should be a parseable Guid");
|
||||||
var evt = writer.Events[0];
|
var evt = writer.Events[0];
|
||||||
Assert.NotNull(evt.CorrelationId);
|
Assert.NotNull(evt.CorrelationId);
|
||||||
Assert.Equal(expected, evt.CorrelationId);
|
Assert.Equal(expected, evt.CorrelationId);
|
||||||
|
Assert.Equal(TestExecutionId, evt.ExecutionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user