From 6b16a4888604373388b18d63d6850a9ac24d389a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 21 May 2026 14:53:08 -0400 Subject: [PATCH] feat(auditlog): ExecutionId on site SQLite schema + gRPC AuditEventDto --- .../Site/SqliteAuditWriter.cs | 19 ++- .../Grpc/AuditEventDtoMapper.cs | 2 + .../Protos/sitestream.proto | 1 + .../SiteStreamGrpc/Sitestream.cs | 118 ++++++++++++------ .../Site/SqliteAuditWriterSchemaTests.cs | 8 +- .../Site/SqliteAuditWriterWriteTests.cs | 33 +++++ .../AuditEventDtoMapperTests.cs | 6 + 7 files changed, 139 insertions(+), 48 deletions(-) diff --git a/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs b/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs index 3bce65c..cf2b216 100644 --- a/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs +++ b/src/ScadaLink.AuditLog/Site/SqliteAuditWriter.cs @@ -114,6 +114,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable PayloadTruncated INTEGER NOT NULL, Extra TEXT NULL, ForwardState TEXT NOT NULL, + ExecutionId TEXT NULL, PRIMARY KEY (EventId) ); CREATE INDEX IF NOT EXISTS IX_SiteAuditLog_ForwardState_Occurred @@ -221,12 +222,14 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable EventId, OccurredAtUtc, Channel, Kind, CorrelationId, SourceSiteId, SourceInstanceId, SourceScript, Actor, Target, Status, HttpStatus, DurationMs, ErrorMessage, ErrorDetail, - RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState + RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState, + ExecutionId ) VALUES ( $EventId, $OccurredAtUtc, $Channel, $Kind, $CorrelationId, $SourceSiteId, $SourceInstanceId, $SourceScript, $Actor, $Target, $Status, $HttpStatus, $DurationMs, $ErrorMessage, $ErrorDetail, - $RequestSummary, $ResponseSummary, $PayloadTruncated, $Extra, $ForwardState + $RequestSummary, $ResponseSummary, $PayloadTruncated, $Extra, $ForwardState, + $ExecutionId ); """; @@ -250,6 +253,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable var pPayloadTruncated = cmd.Parameters.Add("$PayloadTruncated", SqliteType.Integer); var pExtra = cmd.Parameters.Add("$Extra", SqliteType.Text); var pForwardState = cmd.Parameters.Add("$ForwardState", SqliteType.Text); + var pExecutionId = cmd.Parameters.Add("$ExecutionId", SqliteType.Text); foreach (var pending in batch) { @@ -274,6 +278,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable pPayloadTruncated.Value = e.PayloadTruncated ? 1 : 0; pExtra.Value = (object?)e.Extra ?? DBNull.Value; pForwardState.Value = (e.ForwardState ?? AuditForwardState.Pending).ToString(); + pExecutionId.Value = (object?)e.ExecutionId?.ToString() ?? DBNull.Value; try { @@ -331,7 +336,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable SELECT EventId, OccurredAtUtc, Channel, Kind, CorrelationId, SourceSiteId, SourceInstanceId, SourceScript, Actor, Target, Status, HttpStatus, DurationMs, ErrorMessage, ErrorDetail, - RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState + RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState, + ExecutionId FROM AuditLog WHERE ForwardState = $pending ORDER BY OccurredAtUtc ASC, EventId ASC @@ -379,7 +385,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable SELECT EventId, OccurredAtUtc, Channel, Kind, CorrelationId, SourceSiteId, SourceInstanceId, SourceScript, Actor, Target, Status, HttpStatus, DurationMs, ErrorMessage, ErrorDetail, - RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState + RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState, + ExecutionId FROM AuditLog WHERE ForwardState = $forwarded ORDER BY OccurredAtUtc ASC, EventId ASC @@ -465,7 +472,8 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable SELECT EventId, OccurredAtUtc, Channel, Kind, CorrelationId, SourceSiteId, SourceInstanceId, SourceScript, Actor, Target, Status, HttpStatus, DurationMs, ErrorMessage, ErrorDetail, - RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState + RequestSummary, ResponseSummary, PayloadTruncated, Extra, ForwardState, + ExecutionId FROM AuditLog WHERE ForwardState IN ($pending, $forwarded) AND OccurredAtUtc >= $since @@ -642,6 +650,7 @@ public class SqliteAuditWriter : IAuditWriter, ISiteAuditQueue, IAsyncDisposable PayloadTruncated = reader.GetInt32(17) != 0, Extra = reader.IsDBNull(18) ? null : reader.GetString(18), ForwardState = Enum.Parse(reader.GetString(19)), + ExecutionId = reader.IsDBNull(20) ? null : Guid.Parse(reader.GetString(20)), }; } diff --git a/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs b/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs index ed5fb22..4a679e7 100644 --- a/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs +++ b/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs @@ -47,6 +47,7 @@ public static class AuditEventDtoMapper Channel = evt.Channel.ToString(), Kind = evt.Kind.ToString(), CorrelationId = evt.CorrelationId?.ToString() ?? string.Empty, + ExecutionId = evt.ExecutionId?.ToString() ?? string.Empty, SourceSiteId = evt.SourceSiteId ?? string.Empty, SourceInstanceId = evt.SourceInstanceId ?? string.Empty, SourceScript = evt.SourceScript ?? string.Empty, @@ -92,6 +93,7 @@ public static class AuditEventDtoMapper Channel = Enum.Parse(dto.Channel), Kind = Enum.Parse(dto.Kind), CorrelationId = NullIfEmpty(dto.CorrelationId) is { } cid ? Guid.Parse(cid) : null, + ExecutionId = NullIfEmpty(dto.ExecutionId) is { } eid ? Guid.Parse(eid) : null, SourceSiteId = NullIfEmpty(dto.SourceSiteId), SourceInstanceId = NullIfEmpty(dto.SourceInstanceId), SourceScript = NullIfEmpty(dto.SourceScript), diff --git a/src/ScadaLink.Communication/Protos/sitestream.proto b/src/ScadaLink.Communication/Protos/sitestream.proto index 5ceb709..9c671e9 100644 --- a/src/ScadaLink.Communication/Protos/sitestream.proto +++ b/src/ScadaLink.Communication/Protos/sitestream.proto @@ -91,6 +91,7 @@ message AuditEventDto { string response_summary = 17; bool payload_truncated = 18; string extra = 19; + string execution_id = 20; // empty string represents null } message AuditEventBatch { repeated AuditEventDto events = 1; } diff --git a/src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs b/src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs index ccac2bb..d591e78 100644 --- a/src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs +++ b/src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs @@ -41,7 +41,7 @@ namespace ScadaLink.Communication.Grpc { "c3RhdGUYAyABKA4yGi5zaXRlc3RyZWFtLkFsYXJtU3RhdGVFbnVtEhAKCHBy", "aW9yaXR5GAQgASgFEi0KCXRpbWVzdGFtcBgFIAEoCzIaLmdvb2dsZS5wcm90", "b2J1Zi5UaW1lc3RhbXASKQoFbGV2ZWwYBiABKA4yGi5zaXRlc3RyZWFtLkFs", - "YXJtTGV2ZWxFbnVtEg8KB21lc3NhZ2UYByABKAki9QMKDUF1ZGl0RXZlbnRE", + "YXJtTGV2ZWxFbnVtEg8KB21lc3NhZ2UYByABKAkiiwQKDUF1ZGl0RXZlbnRE", "dG8SEAoIZXZlbnRfaWQYASABKAkSMwoPb2NjdXJyZWRfYXRfdXRjGAIgASgL", "MhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdjaGFubmVsGAMgASgJ", "EgwKBGtpbmQYBCABKAkSFgoOY29ycmVsYXRpb25faWQYBSABKAkSFgoOc291", @@ -52,43 +52,43 @@ namespace ScadaLink.Communication.Grpc { "GA0gASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMyVmFsdWUSFQoNZXJyb3Jf", "bWVzc2FnZRgOIAEoCRIUCgxlcnJvcl9kZXRhaWwYDyABKAkSFwoPcmVxdWVz", "dF9zdW1tYXJ5GBAgASgJEhgKEHJlc3BvbnNlX3N1bW1hcnkYESABKAkSGQoR", - "cGF5bG9hZF90cnVuY2F0ZWQYEiABKAgSDQoFZXh0cmEYEyABKAkiPAoPQXVk", - "aXRFdmVudEJhdGNoEikKBmV2ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVk", - "aXRFdmVudER0byInCglJbmdlc3RBY2sSGgoSYWNjZXB0ZWRfZXZlbnRfaWRz", - "GAEgAygJIvQCChZTaXRlQ2FsbE9wZXJhdGlvbmFsRHRvEhwKFHRyYWNrZWRf", - "b3BlcmF0aW9uX2lkGAEgASgJEg8KB2NoYW5uZWwYAiABKAkSDgoGdGFyZ2V0", - "GAMgASgJEhMKC3NvdXJjZV9zaXRlGAQgASgJEg4KBnN0YXR1cxgFIAEoCRIT", - "CgtyZXRyeV9jb3VudBgGIAEoBRISCgpsYXN0X2Vycm9yGAcgASgJEjAKC2h0", - "dHBfc3RhdHVzGAggASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMyVmFsdWUS", - "MgoOY3JlYXRlZF9hdF91dGMYCSABKAsyGi5nb29nbGUucHJvdG9idWYuVGlt", - "ZXN0YW1wEjIKDnVwZGF0ZWRfYXRfdXRjGAogASgLMhouZ29vZ2xlLnByb3Rv", - "YnVmLlRpbWVzdGFtcBIzCg90ZXJtaW5hbF9hdF91dGMYCyABKAsyGi5nb29n", - "bGUucHJvdG9idWYuVGltZXN0YW1wIoABChVDYWNoZWRUZWxlbWV0cnlQYWNr", - "ZXQSLgoLYXVkaXRfZXZlbnQYASABKAsyGS5zaXRlc3RyZWFtLkF1ZGl0RXZl", - "bnREdG8SNwoLb3BlcmF0aW9uYWwYAiABKAsyIi5zaXRlc3RyZWFtLlNpdGVD", - "YWxsT3BlcmF0aW9uYWxEdG8iSgoUQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gSMgoH", - "cGFja2V0cxgBIAMoCzIhLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5UGFj", - "a2V0IlsKFlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QSLQoJc2luY2VfdXRjGAEg", - "ASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpiYXRjaF9zaXpl", - "GAIgASgFIlwKF1B1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEikKBmV2ZW50cxgB", - "IAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxIWCg5tb3JlX2F2YWls", - "YWJsZRgCIAEoCCpcCgdRdWFsaXR5EhcKE1FVQUxJVFlfVU5TUEVDSUZJRUQQ", - "ABIQCgxRVUFMSVRZX0dPT0QQARIVChFRVUFMSVRZX1VOQ0VSVEFJThACEg8K", - "C1FVQUxJVFlfQkFEEAMqXQoOQWxhcm1TdGF0ZUVudW0SGwoXQUxBUk1fU1RB", - "VEVfVU5TUEVDSUZJRUQQABIWChJBTEFSTV9TVEFURV9OT1JNQUwQARIWChJB", - "TEFSTV9TVEFURV9BQ1RJVkUQAiqFAQoOQWxhcm1MZXZlbEVudW0SFAoQQUxB", - "Uk1fTEVWRUxfTk9ORRAAEhMKD0FMQVJNX0xFVkVMX0xPVxABEhcKE0FMQVJN", - "X0xFVkVMX0xPV19MT1cQAhIUChBBTEFSTV9MRVZFTF9ISUdIEAMSGQoVQUxB", - "Uk1fTEVWRUxfSElHSF9ISUdIEAQy4QIKEVNpdGVTdHJlYW1TZXJ2aWNlElUK", - "EVN1YnNjcmliZUluc3RhbmNlEiEuc2l0ZXN0cmVhbS5JbnN0YW5jZVN0cmVh", - "bVJlcXVlc3QaGy5zaXRlc3RyZWFtLlNpdGVTdHJlYW1FdmVudDABEkcKEUlu", - "Z2VzdEF1ZGl0RXZlbnRzEhsuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50QmF0Y2ga", - "FS5zaXRlc3RyZWFtLkluZ2VzdEFjaxJQChVJbmdlc3RDYWNoZWRUZWxlbWV0", - "cnkSIC5zaXRlc3RyZWFtLkNhY2hlZFRlbGVtZXRyeUJhdGNoGhUuc2l0ZXN0", - "cmVhbS5Jbmdlc3RBY2sSWgoPUHVsbEF1ZGl0RXZlbnRzEiIuc2l0ZXN0cmVh", - "bS5QdWxsQXVkaXRFdmVudHNSZXF1ZXN0GiMuc2l0ZXN0cmVhbS5QdWxsQXVk", - "aXRFdmVudHNSZXNwb25zZUIfqgIcU2NhZGFMaW5rLkNvbW11bmljYXRpb24u", - "R3JwY2IGcHJvdG8z")); + "cGF5bG9hZF90cnVuY2F0ZWQYEiABKAgSDQoFZXh0cmEYEyABKAkSFAoMZXhl", + "Y3V0aW9uX2lkGBQgASgJIjwKD0F1ZGl0RXZlbnRCYXRjaBIpCgZldmVudHMY", + "ASADKAsyGS5zaXRlc3RyZWFtLkF1ZGl0RXZlbnREdG8iJwoJSW5nZXN0QWNr", + "EhoKEmFjY2VwdGVkX2V2ZW50X2lkcxgBIAMoCSL0AgoWU2l0ZUNhbGxPcGVy", + "YXRpb25hbER0bxIcChR0cmFja2VkX29wZXJhdGlvbl9pZBgBIAEoCRIPCgdj", + "aGFubmVsGAIgASgJEg4KBnRhcmdldBgDIAEoCRITCgtzb3VyY2Vfc2l0ZRgE", + "IAEoCRIOCgZzdGF0dXMYBSABKAkSEwoLcmV0cnlfY291bnQYBiABKAUSEgoK", + "bGFzdF9lcnJvchgHIAEoCRIwCgtodHRwX3N0YXR1cxgIIAEoCzIbLmdvb2ds", + "ZS5wcm90b2J1Zi5JbnQzMlZhbHVlEjIKDmNyZWF0ZWRfYXRfdXRjGAkgASgL", + "MhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIyCg51cGRhdGVkX2F0X3V0", + "YxgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASMwoPdGVybWlu", + "YWxfYXRfdXRjGAsgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCKA", + "AQoVQ2FjaGVkVGVsZW1ldHJ5UGFja2V0Ei4KC2F1ZGl0X2V2ZW50GAEgASgL", + "Mhkuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50RHRvEjcKC29wZXJhdGlvbmFsGAIg", + "ASgLMiIuc2l0ZXN0cmVhbS5TaXRlQ2FsbE9wZXJhdGlvbmFsRHRvIkoKFENh", + "Y2hlZFRlbGVtZXRyeUJhdGNoEjIKB3BhY2tldHMYASADKAsyIS5zaXRlc3Ry", + "ZWFtLkNhY2hlZFRlbGVtZXRyeVBhY2tldCJbChZQdWxsQXVkaXRFdmVudHNS", + "ZXF1ZXN0Ei0KCXNpbmNlX3V0YxgBIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5U", + "aW1lc3RhbXASEgoKYmF0Y2hfc2l6ZRgCIAEoBSJcChdQdWxsQXVkaXRFdmVu", + "dHNSZXNwb25zZRIpCgZldmVudHMYASADKAsyGS5zaXRlc3RyZWFtLkF1ZGl0", + "RXZlbnREdG8SFgoObW9yZV9hdmFpbGFibGUYAiABKAgqXAoHUXVhbGl0eRIX", + "ChNRVUFMSVRZX1VOU1BFQ0lGSUVEEAASEAoMUVVBTElUWV9HT09EEAESFQoR", + "UVVBTElUWV9VTkNFUlRBSU4QAhIPCgtRVUFMSVRZX0JBRBADKl0KDkFsYXJt", + "U3RhdGVFbnVtEhsKF0FMQVJNX1NUQVRFX1VOU1BFQ0lGSUVEEAASFgoSQUxB", + "Uk1fU1RBVEVfTk9STUFMEAESFgoSQUxBUk1fU1RBVEVfQUNUSVZFEAIqhQEK", + "DkFsYXJtTGV2ZWxFbnVtEhQKEEFMQVJNX0xFVkVMX05PTkUQABITCg9BTEFS", + "TV9MRVZFTF9MT1cQARIXChNBTEFSTV9MRVZFTF9MT1dfTE9XEAISFAoQQUxB", + "Uk1fTEVWRUxfSElHSBADEhkKFUFMQVJNX0xFVkVMX0hJR0hfSElHSBAEMuEC", + "ChFTaXRlU3RyZWFtU2VydmljZRJVChFTdWJzY3JpYmVJbnN0YW5jZRIhLnNp", + "dGVzdHJlYW0uSW5zdGFuY2VTdHJlYW1SZXF1ZXN0Ghsuc2l0ZXN0cmVhbS5T", + "aXRlU3RyZWFtRXZlbnQwARJHChFJbmdlc3RBdWRpdEV2ZW50cxIbLnNpdGVz", + "dHJlYW0uQXVkaXRFdmVudEJhdGNoGhUuc2l0ZXN0cmVhbS5Jbmdlc3RBY2sS", + "UAoVSW5nZXN0Q2FjaGVkVGVsZW1ldHJ5EiAuc2l0ZXN0cmVhbS5DYWNoZWRU", + "ZWxlbWV0cnlCYXRjaBoVLnNpdGVzdHJlYW0uSW5nZXN0QWNrEloKD1B1bGxB", + "dWRpdEV2ZW50cxIiLnNpdGVzdHJlYW0uUHVsbEF1ZGl0RXZlbnRzUmVxdWVz", + "dBojLnNpdGVzdHJlYW0uUHVsbEF1ZGl0RXZlbnRzUmVzcG9uc2VCH6oCHFNj", + "YWRhTGluay5Db21tdW5pY2F0aW9uLkdycGNiBnByb3RvMw==")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.WrappersReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(new[] {typeof(global::ScadaLink.Communication.Grpc.Quality), typeof(global::ScadaLink.Communication.Grpc.AlarmStateEnum), typeof(global::ScadaLink.Communication.Grpc.AlarmLevelEnum), }, null, new pbr::GeneratedClrTypeInfo[] { @@ -96,7 +96,7 @@ namespace ScadaLink.Communication.Grpc { new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.SiteStreamEvent), global::ScadaLink.Communication.Grpc.SiteStreamEvent.Parser, new[]{ "CorrelationId", "AttributeChanged", "AlarmChanged" }, new[]{ "Event" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AttributeValueUpdate), global::ScadaLink.Communication.Grpc.AttributeValueUpdate.Parser, new[]{ "InstanceUniqueName", "AttributePath", "AttributeName", "Value", "Quality", "Timestamp" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AlarmStateUpdate), global::ScadaLink.Communication.Grpc.AlarmStateUpdate.Parser, new[]{ "InstanceUniqueName", "AlarmName", "State", "Priority", "Timestamp", "Level", "Message" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AuditEventDto), global::ScadaLink.Communication.Grpc.AuditEventDto.Parser, new[]{ "EventId", "OccurredAtUtc", "Channel", "Kind", "CorrelationId", "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AuditEventDto), global::ScadaLink.Communication.Grpc.AuditEventDto.Parser, new[]{ "EventId", "OccurredAtUtc", "Channel", "Kind", "CorrelationId", "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra", "ExecutionId" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.AuditEventBatch), global::ScadaLink.Communication.Grpc.AuditEventBatch.Parser, new[]{ "Events" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.IngestAck), global::ScadaLink.Communication.Grpc.IngestAck.Parser, new[]{ "AcceptedEventIds" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.SiteCallOperationalDto), global::ScadaLink.Communication.Grpc.SiteCallOperationalDto.Parser, new[]{ "TrackedOperationId", "Channel", "Target", "SourceSite", "Status", "RetryCount", "LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc" }, null, null, null, null), @@ -1591,6 +1591,7 @@ namespace ScadaLink.Communication.Grpc { responseSummary_ = other.responseSummary_; payloadTruncated_ = other.payloadTruncated_; extra_ = other.extra_; + executionId_ = other.executionId_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -1838,6 +1839,21 @@ namespace ScadaLink.Communication.Grpc { } } + /// Field number for the "execution_id" field. + public const int ExecutionIdFieldNumber = 20; + private string executionId_ = ""; + /// + /// empty string represents null + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string ExecutionId { + get { return executionId_; } + set { + executionId_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -1872,6 +1888,7 @@ namespace ScadaLink.Communication.Grpc { if (ResponseSummary != other.ResponseSummary) return false; if (PayloadTruncated != other.PayloadTruncated) return false; if (Extra != other.Extra) return false; + if (ExecutionId != other.ExecutionId) return false; return Equals(_unknownFields, other._unknownFields); } @@ -1898,6 +1915,7 @@ namespace ScadaLink.Communication.Grpc { if (ResponseSummary.Length != 0) hash ^= ResponseSummary.GetHashCode(); if (PayloadTruncated != false) hash ^= PayloadTruncated.GetHashCode(); if (Extra.Length != 0) hash ^= Extra.GetHashCode(); + if (ExecutionId.Length != 0) hash ^= ExecutionId.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -1990,6 +2008,10 @@ namespace ScadaLink.Communication.Grpc { output.WriteRawTag(154, 1); output.WriteString(Extra); } + if (ExecutionId.Length != 0) { + output.WriteRawTag(162, 1); + output.WriteString(ExecutionId); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -2074,6 +2096,10 @@ namespace ScadaLink.Communication.Grpc { output.WriteRawTag(154, 1); output.WriteString(Extra); } + if (ExecutionId.Length != 0) { + output.WriteRawTag(162, 1); + output.WriteString(ExecutionId); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -2141,6 +2167,9 @@ namespace ScadaLink.Communication.Grpc { if (Extra.Length != 0) { size += 2 + pb::CodedOutputStream.ComputeStringSize(Extra); } + if (ExecutionId.Length != 0) { + size += 2 + pb::CodedOutputStream.ComputeStringSize(ExecutionId); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -2217,6 +2246,9 @@ namespace ScadaLink.Communication.Grpc { if (other.Extra.Length != 0) { Extra = other.Extra; } + if (other.ExecutionId.Length != 0) { + ExecutionId = other.ExecutionId; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -2321,6 +2353,10 @@ namespace ScadaLink.Communication.Grpc { Extra = input.ReadString(); break; } + case 162: { + ExecutionId = input.ReadString(); + break; + } } } #endif @@ -2425,6 +2461,10 @@ namespace ScadaLink.Communication.Grpc { Extra = input.ReadString(); break; } + case 162: { + ExecutionId = input.ReadString(); + break; + } } } } diff --git a/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterSchemaTests.cs b/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterSchemaTests.cs index b02dd63..e6015fd 100644 --- a/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterSchemaTests.cs +++ b/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterSchemaTests.cs @@ -41,9 +41,9 @@ public class SqliteAuditWriterSchemaTests } [Fact] - public void Opens_Creates_AuditLog_Table_With_20Columns_And_PK_On_EventId() + public void Opens_Creates_AuditLog_Table_With_21Columns_And_PK_On_EventId() { - var (writer, dataSource) = CreateWriter(nameof(Opens_Creates_AuditLog_Table_With_20Columns_And_PK_On_EventId)); + var (writer, dataSource) = CreateWriter(nameof(Opens_Creates_AuditLog_Table_With_21Columns_And_PK_On_EventId)); using (writer) { using var connection = OpenVerifierConnection(dataSource); @@ -57,7 +57,7 @@ public class SqliteAuditWriterSchemaTests columns.Add((reader.GetString(1), reader.GetInt32(5))); } - Assert.Equal(20, columns.Count); + Assert.Equal(21, columns.Count); var expected = new[] { @@ -65,7 +65,7 @@ public class SqliteAuditWriterSchemaTests "SourceSiteId", "SourceInstanceId", "SourceScript", "Actor", "Target", "Status", "HttpStatus", "DurationMs", "ErrorMessage", "ErrorDetail", "RequestSummary", "ResponseSummary", "PayloadTruncated", "Extra", - "ForwardState", + "ForwardState", "ExecutionId", }; Assert.Equal(expected.OrderBy(n => n), columns.Select(c => c.Name).OrderBy(n => n)); diff --git a/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterWriteTests.cs b/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterWriteTests.cs index f9fe5c4..58dc32c 100644 --- a/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterWriteTests.cs +++ b/tests/ScadaLink.AuditLog.Tests/Site/SqliteAuditWriterWriteTests.cs @@ -353,4 +353,37 @@ public class SqliteAuditWriterWriteTests await writer.MarkReconciledAsync(new[] { Guid.NewGuid(), Guid.NewGuid() }); // Completes without throwing. } + + // ----- ExecutionId column (universal per-run correlation value) ----- // + + [Fact] + public async Task WriteAsync_NonNullExecutionId_RoundTripsThroughMapRow() + { + var (writer, _) = CreateWriter(nameof(WriteAsync_NonNullExecutionId_RoundTripsThroughMapRow)); + await using var _w = writer; + + var executionId = Guid.NewGuid(); + var evt = NewEvent() with { ExecutionId = executionId }; + await writer.WriteAsync(evt); + + var rows = await writer.ReadPendingAsync(limit: 10); + + var row = Assert.Single(rows); + Assert.Equal(executionId, row.ExecutionId); + } + + [Fact] + public async Task WriteAsync_NullExecutionId_RoundTripsAsNull() + { + var (writer, _) = CreateWriter(nameof(WriteAsync_NullExecutionId_RoundTripsAsNull)); + await using var _w = writer; + + var evt = NewEvent() with { ExecutionId = null }; + await writer.WriteAsync(evt); + + var rows = await writer.ReadPendingAsync(limit: 10); + + var row = Assert.Single(rows); + Assert.Null(row.ExecutionId); + } } diff --git a/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs b/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs index a247ec6..c741855 100644 --- a/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs +++ b/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs @@ -19,6 +19,7 @@ public class AuditEventDtoMapperTests var occurredAt = new DateTime(2026, 5, 20, 10, 15, 30, 123, DateTimeKind.Utc); var ingestedAt = new DateTime(2026, 5, 20, 10, 15, 31, 0, DateTimeKind.Utc); var correlationId = Guid.NewGuid(); + var executionId = Guid.NewGuid(); var eventId = Guid.NewGuid(); var original = new AuditEvent @@ -29,6 +30,7 @@ public class AuditEventDtoMapperTests Channel = AuditChannel.ApiOutbound, Kind = AuditKind.ApiCallCached, CorrelationId = correlationId, + ExecutionId = executionId, SourceSiteId = "site-1", SourceInstanceId = "Pump01", SourceScript = "OnDemand", @@ -54,6 +56,7 @@ public class AuditEventDtoMapperTests Assert.Equal(original.Channel, roundTripped.Channel); Assert.Equal(original.Kind, roundTripped.Kind); Assert.Equal(original.CorrelationId, roundTripped.CorrelationId); + Assert.Equal(original.ExecutionId, roundTripped.ExecutionId); Assert.Equal(original.SourceSiteId, roundTripped.SourceSiteId); Assert.Equal(original.SourceInstanceId, roundTripped.SourceInstanceId); Assert.Equal(original.SourceScript, roundTripped.SourceScript); @@ -90,6 +93,7 @@ public class AuditEventDtoMapperTests var dto = AuditEventDtoMapper.ToDto(evt); Assert.Equal(string.Empty, dto.CorrelationId); + Assert.Equal(string.Empty, dto.ExecutionId); Assert.Equal(string.Empty, dto.SourceSiteId); Assert.Equal(string.Empty, dto.SourceInstanceId); Assert.Equal(string.Empty, dto.SourceScript); @@ -113,6 +117,7 @@ public class AuditEventDtoMapperTests Kind = nameof(AuditKind.ApiCall), Status = nameof(AuditStatus.Submitted), CorrelationId = string.Empty, + ExecutionId = string.Empty, SourceSiteId = string.Empty, SourceInstanceId = string.Empty, SourceScript = string.Empty, @@ -128,6 +133,7 @@ public class AuditEventDtoMapperTests var evt = AuditEventDtoMapper.FromDto(dto); Assert.Null(evt.CorrelationId); + Assert.Null(evt.ExecutionId); Assert.Null(evt.SourceSiteId); Assert.Null(evt.SourceInstanceId); Assert.Null(evt.SourceScript);