From dfaa416ebe8e49cace562608a12416f7a4ae2fc1 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 23 May 2026 16:10:03 -0400 Subject: [PATCH] feat(comm): add source_node field to AuditEventDto + SiteCallOperationalDto proto - AuditEventDto field 22, SiteCallOperationalDto field 12. Both follow the existing empty-string-means-null convention. - Mappers carry SourceNode end-to-end; round-trip tests cover both populated and null cases. --- .../Grpc/AuditEventDtoMapper.cs | 2 + .../Grpc/SiteCallDtoMapper.cs | 1 + .../Protos/sitestream.proto | 2 + .../SiteStreamGrpc/Sitestream.cs | 161 +++++++++++++----- .../CombinedTelemetryDispatcher.cs | 1 + .../AuditEventDtoMapperTests.cs | 53 ++++++ .../Protos/AuditEventProtoTests.cs | 2 + .../Protos/CachedTelemetryProtoTests.cs | 2 + .../SiteCallDtoMapperTests.cs | 37 ++++ 9 files changed, 221 insertions(+), 40 deletions(-) diff --git a/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs b/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs index 640cb13..a2fe2ee 100644 --- a/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs +++ b/src/ScadaLink.Communication/Grpc/AuditEventDtoMapper.cs @@ -50,6 +50,7 @@ public static class AuditEventDtoMapper ExecutionId = evt.ExecutionId?.ToString() ?? string.Empty, ParentExecutionId = evt.ParentExecutionId?.ToString() ?? string.Empty, SourceSiteId = evt.SourceSiteId ?? string.Empty, + SourceNode = evt.SourceNode ?? string.Empty, SourceInstanceId = evt.SourceInstanceId ?? string.Empty, SourceScript = evt.SourceScript ?? string.Empty, Actor = evt.Actor ?? string.Empty, @@ -97,6 +98,7 @@ public static class AuditEventDtoMapper ExecutionId = NullIfEmpty(dto.ExecutionId) is { } eid ? Guid.Parse(eid) : null, ParentExecutionId = NullIfEmpty(dto.ParentExecutionId) is { } pid ? Guid.Parse(pid) : null, SourceSiteId = NullIfEmpty(dto.SourceSiteId), + SourceNode = NullIfEmpty(dto.SourceNode), SourceInstanceId = NullIfEmpty(dto.SourceInstanceId), SourceScript = NullIfEmpty(dto.SourceScript), Actor = NullIfEmpty(dto.Actor), diff --git a/src/ScadaLink.Communication/Grpc/SiteCallDtoMapper.cs b/src/ScadaLink.Communication/Grpc/SiteCallDtoMapper.cs index c61e3e5..c3e7ab3 100644 --- a/src/ScadaLink.Communication/Grpc/SiteCallDtoMapper.cs +++ b/src/ScadaLink.Communication/Grpc/SiteCallDtoMapper.cs @@ -55,6 +55,7 @@ public static class SiteCallDtoMapper Channel = dto.Channel, Target = dto.Target, SourceSite = dto.SourceSite, + SourceNode = string.IsNullOrEmpty(dto.SourceNode) ? null : dto.SourceNode, Status = dto.Status, RetryCount = dto.RetryCount, LastError = string.IsNullOrEmpty(dto.LastError) ? null : dto.LastError, diff --git a/src/ScadaLink.Communication/Protos/sitestream.proto b/src/ScadaLink.Communication/Protos/sitestream.proto index dccad81..1d8e4b2 100644 --- a/src/ScadaLink.Communication/Protos/sitestream.proto +++ b/src/ScadaLink.Communication/Protos/sitestream.proto @@ -93,6 +93,7 @@ message AuditEventDto { string extra = 19; string execution_id = 20; // empty string represents null string parent_execution_id = 21; // empty string represents null + string source_node = 22; // empty string represents null } message AuditEventBatch { repeated AuditEventDto events = 1; } @@ -114,6 +115,7 @@ message SiteCallOperationalDto { google.protobuf.Timestamp created_at_utc = 9; google.protobuf.Timestamp updated_at_utc = 10; google.protobuf.Timestamp terminal_at_utc = 11; // absent when not terminal + string source_node = 12; // empty string represents null } message CachedTelemetryPacket { diff --git a/src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs b/src/ScadaLink.Communication/SiteStreamGrpc/Sitestream.cs index 41e11e4..0732cde 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", - "YXJtTGV2ZWxFbnVtEg8KB21lc3NhZ2UYByABKAkiqAQKDUF1ZGl0RXZlbnRE", + "YXJtTGV2ZWxFbnVtEg8KB21lc3NhZ2UYByABKAkivQQKDUF1ZGl0RXZlbnRE", "dG8SEAoIZXZlbnRfaWQYASABKAkSMwoPb2NjdXJyZWRfYXRfdXRjGAIgASgL", "MhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIPCgdjaGFubmVsGAMgASgJ", "EgwKBGtpbmQYBCABKAkSFgoOY29ycmVsYXRpb25faWQYBSABKAkSFgoOc291", @@ -53,43 +53,44 @@ namespace ScadaLink.Communication.Grpc { "bWVzc2FnZRgOIAEoCRIUCgxlcnJvcl9kZXRhaWwYDyABKAkSFwoPcmVxdWVz", "dF9zdW1tYXJ5GBAgASgJEhgKEHJlc3BvbnNlX3N1bW1hcnkYESABKAkSGQoR", "cGF5bG9hZF90cnVuY2F0ZWQYEiABKAgSDQoFZXh0cmEYEyABKAkSFAoMZXhl", - "Y3V0aW9uX2lkGBQgASgJEhsKE3BhcmVudF9leGVjdXRpb25faWQYFSABKAki", - "PAoPQXVkaXRFdmVudEJhdGNoEikKBmV2ZW50cxgBIAMoCzIZLnNpdGVzdHJl", - "YW0uQXVkaXRFdmVudER0byInCglJbmdlc3RBY2sSGgoSYWNjZXB0ZWRfZXZl", - "bnRfaWRzGAEgAygJIvQCChZTaXRlQ2FsbE9wZXJhdGlvbmFsRHRvEhwKFHRy", - "YWNrZWRfb3BlcmF0aW9uX2lkGAEgASgJEg8KB2NoYW5uZWwYAiABKAkSDgoG", - "dGFyZ2V0GAMgASgJEhMKC3NvdXJjZV9zaXRlGAQgASgJEg4KBnN0YXR1cxgF", - "IAEoCRITCgtyZXRyeV9jb3VudBgGIAEoBRISCgpsYXN0X2Vycm9yGAcgASgJ", - "EjAKC2h0dHBfc3RhdHVzGAggASgLMhsuZ29vZ2xlLnByb3RvYnVmLkludDMy", - "VmFsdWUSMgoOY3JlYXRlZF9hdF91dGMYCSABKAsyGi5nb29nbGUucHJvdG9i", - "dWYuVGltZXN0YW1wEjIKDnVwZGF0ZWRfYXRfdXRjGAogASgLMhouZ29vZ2xl", - "LnByb3RvYnVmLlRpbWVzdGFtcBIzCg90ZXJtaW5hbF9hdF91dGMYCyABKAsy", - "Gi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wIoABChVDYWNoZWRUZWxlbWV0", - "cnlQYWNrZXQSLgoLYXVkaXRfZXZlbnQYASABKAsyGS5zaXRlc3RyZWFtLkF1", - "ZGl0RXZlbnREdG8SNwoLb3BlcmF0aW9uYWwYAiABKAsyIi5zaXRlc3RyZWFt", - "LlNpdGVDYWxsT3BlcmF0aW9uYWxEdG8iSgoUQ2FjaGVkVGVsZW1ldHJ5QmF0", - "Y2gSMgoHcGFja2V0cxgBIAMoCzIhLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1l", - "dHJ5UGFja2V0IlsKFlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QSLQoJc2luY2Vf", - "dXRjGAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpiYXRj", - "aF9zaXplGAIgASgFIlwKF1B1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEikKBmV2", - "ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxIWCg5tb3Jl", - "X2F2YWlsYWJsZRgCIAEoCCpcCgdRdWFsaXR5EhcKE1FVQUxJVFlfVU5TUEVD", - "SUZJRUQQABIQCgxRVUFMSVRZX0dPT0QQARIVChFRVUFMSVRZX1VOQ0VSVEFJ", - "ThACEg8KC1FVQUxJVFlfQkFEEAMqXQoOQWxhcm1TdGF0ZUVudW0SGwoXQUxB", - "Uk1fU1RBVEVfVU5TUEVDSUZJRUQQABIWChJBTEFSTV9TVEFURV9OT1JNQUwQ", - "ARIWChJBTEFSTV9TVEFURV9BQ1RJVkUQAiqFAQoOQWxhcm1MZXZlbEVudW0S", - "FAoQQUxBUk1fTEVWRUxfTk9ORRAAEhMKD0FMQVJNX0xFVkVMX0xPVxABEhcK", - "E0FMQVJNX0xFVkVMX0xPV19MT1cQAhIUChBBTEFSTV9MRVZFTF9ISUdIEAMS", - "GQoVQUxBUk1fTEVWRUxfSElHSF9ISUdIEAQy4QIKEVNpdGVTdHJlYW1TZXJ2", - "aWNlElUKEVN1YnNjcmliZUluc3RhbmNlEiEuc2l0ZXN0cmVhbS5JbnN0YW5j", - "ZVN0cmVhbVJlcXVlc3QaGy5zaXRlc3RyZWFtLlNpdGVTdHJlYW1FdmVudDAB", - "EkcKEUluZ2VzdEF1ZGl0RXZlbnRzEhsuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50", - "QmF0Y2gaFS5zaXRlc3RyZWFtLkluZ2VzdEFjaxJQChVJbmdlc3RDYWNoZWRU", - "ZWxlbWV0cnkSIC5zaXRlc3RyZWFtLkNhY2hlZFRlbGVtZXRyeUJhdGNoGhUu", - "c2l0ZXN0cmVhbS5Jbmdlc3RBY2sSWgoPUHVsbEF1ZGl0RXZlbnRzEiIuc2l0", - "ZXN0cmVhbS5QdWxsQXVkaXRFdmVudHNSZXF1ZXN0GiMuc2l0ZXN0cmVhbS5Q", - "dWxsQXVkaXRFdmVudHNSZXNwb25zZUIfqgIcU2NhZGFMaW5rLkNvbW11bmlj", - "YXRpb24uR3JwY2IGcHJvdG8z")); + "Y3V0aW9uX2lkGBQgASgJEhsKE3BhcmVudF9leGVjdXRpb25faWQYFSABKAkS", + "EwoLc291cmNlX25vZGUYFiABKAkiPAoPQXVkaXRFdmVudEJhdGNoEikKBmV2", + "ZW50cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0byInCglJbmdl", + "c3RBY2sSGgoSYWNjZXB0ZWRfZXZlbnRfaWRzGAEgAygJIokDChZTaXRlQ2Fs", + "bE9wZXJhdGlvbmFsRHRvEhwKFHRyYWNrZWRfb3BlcmF0aW9uX2lkGAEgASgJ", + "Eg8KB2NoYW5uZWwYAiABKAkSDgoGdGFyZ2V0GAMgASgJEhMKC3NvdXJjZV9z", + "aXRlGAQgASgJEg4KBnN0YXR1cxgFIAEoCRITCgtyZXRyeV9jb3VudBgGIAEo", + "BRISCgpsYXN0X2Vycm9yGAcgASgJEjAKC2h0dHBfc3RhdHVzGAggASgLMhsu", + "Z29vZ2xlLnByb3RvYnVmLkludDMyVmFsdWUSMgoOY3JlYXRlZF9hdF91dGMY", + "CSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjIKDnVwZGF0ZWRf", + "YXRfdXRjGAogASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBIzCg90", + "ZXJtaW5hbF9hdF91dGMYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0", + "YW1wEhMKC3NvdXJjZV9ub2RlGAwgASgJIoABChVDYWNoZWRUZWxlbWV0cnlQ", + "YWNrZXQSLgoLYXVkaXRfZXZlbnQYASABKAsyGS5zaXRlc3RyZWFtLkF1ZGl0", + "RXZlbnREdG8SNwoLb3BlcmF0aW9uYWwYAiABKAsyIi5zaXRlc3RyZWFtLlNp", + "dGVDYWxsT3BlcmF0aW9uYWxEdG8iSgoUQ2FjaGVkVGVsZW1ldHJ5QmF0Y2gS", + "MgoHcGFja2V0cxgBIAMoCzIhLnNpdGVzdHJlYW0uQ2FjaGVkVGVsZW1ldHJ5", + "UGFja2V0IlsKFlB1bGxBdWRpdEV2ZW50c1JlcXVlc3QSLQoJc2luY2VfdXRj", + "GAEgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBISCgpiYXRjaF9z", + "aXplGAIgASgFIlwKF1B1bGxBdWRpdEV2ZW50c1Jlc3BvbnNlEikKBmV2ZW50", + "cxgBIAMoCzIZLnNpdGVzdHJlYW0uQXVkaXRFdmVudER0bxIWCg5tb3JlX2F2", + "YWlsYWJsZRgCIAEoCCpcCgdRdWFsaXR5EhcKE1FVQUxJVFlfVU5TUEVDSUZJ", + "RUQQABIQCgxRVUFMSVRZX0dPT0QQARIVChFRVUFMSVRZX1VOQ0VSVEFJThAC", + "Eg8KC1FVQUxJVFlfQkFEEAMqXQoOQWxhcm1TdGF0ZUVudW0SGwoXQUxBUk1f", + "U1RBVEVfVU5TUEVDSUZJRUQQABIWChJBTEFSTV9TVEFURV9OT1JNQUwQARIW", + "ChJBTEFSTV9TVEFURV9BQ1RJVkUQAiqFAQoOQWxhcm1MZXZlbEVudW0SFAoQ", + "QUxBUk1fTEVWRUxfTk9ORRAAEhMKD0FMQVJNX0xFVkVMX0xPVxABEhcKE0FM", + "QVJNX0xFVkVMX0xPV19MT1cQAhIUChBBTEFSTV9MRVZFTF9ISUdIEAMSGQoV", + "QUxBUk1fTEVWRUxfSElHSF9ISUdIEAQy4QIKEVNpdGVTdHJlYW1TZXJ2aWNl", + "ElUKEVN1YnNjcmliZUluc3RhbmNlEiEuc2l0ZXN0cmVhbS5JbnN0YW5jZVN0", + "cmVhbVJlcXVlc3QaGy5zaXRlc3RyZWFtLlNpdGVTdHJlYW1FdmVudDABEkcK", + "EUluZ2VzdEF1ZGl0RXZlbnRzEhsuc2l0ZXN0cmVhbS5BdWRpdEV2ZW50QmF0", + "Y2gaFS5zaXRlc3RyZWFtLkluZ2VzdEFjaxJQChVJbmdlc3RDYWNoZWRUZWxl", + "bWV0cnkSIC5zaXRlc3RyZWFtLkNhY2hlZFRlbGVtZXRyeUJhdGNoGhUuc2l0", + "ZXN0cmVhbS5Jbmdlc3RBY2sSWgoPUHVsbEF1ZGl0RXZlbnRzEiIuc2l0ZXN0", + "cmVhbS5QdWxsQXVkaXRFdmVudHNSZXF1ZXN0GiMuc2l0ZXN0cmVhbS5QdWxs", + "QXVkaXRFdmVudHNSZXNwb25zZUIfqgIcU2NhZGFMaW5rLkNvbW11bmljYXRp", + "b24uR3JwY2IGcHJvdG8z")); 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[] { @@ -97,10 +98,10 @@ 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", "ExecutionId", "ParentExecutionId" }, 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", "ParentExecutionId", "SourceNode" }, 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), + 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", "SourceNode" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.CachedTelemetryPacket), global::ScadaLink.Communication.Grpc.CachedTelemetryPacket.Parser, new[]{ "AuditEvent", "Operational" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.CachedTelemetryBatch), global::ScadaLink.Communication.Grpc.CachedTelemetryBatch.Parser, new[]{ "Packets" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::ScadaLink.Communication.Grpc.PullAuditEventsRequest), global::ScadaLink.Communication.Grpc.PullAuditEventsRequest.Parser, new[]{ "SinceUtc", "BatchSize" }, null, null, null, null), @@ -1594,6 +1595,7 @@ namespace ScadaLink.Communication.Grpc { extra_ = other.extra_; executionId_ = other.executionId_; parentExecutionId_ = other.parentExecutionId_; + sourceNode_ = other.sourceNode_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -1871,6 +1873,21 @@ namespace ScadaLink.Communication.Grpc { } } + /// Field number for the "source_node" field. + public const int SourceNodeFieldNumber = 22; + private string sourceNode_ = ""; + /// + /// empty string represents null + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string SourceNode { + get { return sourceNode_; } + set { + sourceNode_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -1907,6 +1924,7 @@ namespace ScadaLink.Communication.Grpc { if (Extra != other.Extra) return false; if (ExecutionId != other.ExecutionId) return false; if (ParentExecutionId != other.ParentExecutionId) return false; + if (SourceNode != other.SourceNode) return false; return Equals(_unknownFields, other._unknownFields); } @@ -1935,6 +1953,7 @@ namespace ScadaLink.Communication.Grpc { if (Extra.Length != 0) hash ^= Extra.GetHashCode(); if (ExecutionId.Length != 0) hash ^= ExecutionId.GetHashCode(); if (ParentExecutionId.Length != 0) hash ^= ParentExecutionId.GetHashCode(); + if (SourceNode.Length != 0) hash ^= SourceNode.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -2035,6 +2054,10 @@ namespace ScadaLink.Communication.Grpc { output.WriteRawTag(170, 1); output.WriteString(ParentExecutionId); } + if (SourceNode.Length != 0) { + output.WriteRawTag(178, 1); + output.WriteString(SourceNode); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -2127,6 +2150,10 @@ namespace ScadaLink.Communication.Grpc { output.WriteRawTag(170, 1); output.WriteString(ParentExecutionId); } + if (SourceNode.Length != 0) { + output.WriteRawTag(178, 1); + output.WriteString(SourceNode); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -2200,6 +2227,9 @@ namespace ScadaLink.Communication.Grpc { if (ParentExecutionId.Length != 0) { size += 2 + pb::CodedOutputStream.ComputeStringSize(ParentExecutionId); } + if (SourceNode.Length != 0) { + size += 2 + pb::CodedOutputStream.ComputeStringSize(SourceNode); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -2282,6 +2312,9 @@ namespace ScadaLink.Communication.Grpc { if (other.ParentExecutionId.Length != 0) { ParentExecutionId = other.ParentExecutionId; } + if (other.SourceNode.Length != 0) { + SourceNode = other.SourceNode; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -2394,6 +2427,10 @@ namespace ScadaLink.Communication.Grpc { ParentExecutionId = input.ReadString(); break; } + case 178: { + SourceNode = input.ReadString(); + break; + } } } #endif @@ -2506,6 +2543,10 @@ namespace ScadaLink.Communication.Grpc { ParentExecutionId = input.ReadString(); break; } + case 178: { + SourceNode = input.ReadString(); + break; + } } } } @@ -2939,6 +2980,7 @@ namespace ScadaLink.Communication.Grpc { createdAtUtc_ = other.createdAtUtc_ != null ? other.createdAtUtc_.Clone() : null; updatedAtUtc_ = other.updatedAtUtc_ != null ? other.updatedAtUtc_.Clone() : null; terminalAtUtc_ = other.terminalAtUtc_ != null ? other.terminalAtUtc_.Clone() : null; + sourceNode_ = other.sourceNode_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -3097,6 +3139,21 @@ namespace ScadaLink.Communication.Grpc { } } + /// Field number for the "source_node" field. + public const int SourceNodeFieldNumber = 12; + private string sourceNode_ = ""; + /// + /// empty string represents null + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string SourceNode { + get { return sourceNode_; } + set { + sourceNode_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -3123,6 +3180,7 @@ namespace ScadaLink.Communication.Grpc { if (!object.Equals(CreatedAtUtc, other.CreatedAtUtc)) return false; if (!object.Equals(UpdatedAtUtc, other.UpdatedAtUtc)) return false; if (!object.Equals(TerminalAtUtc, other.TerminalAtUtc)) return false; + if (SourceNode != other.SourceNode) return false; return Equals(_unknownFields, other._unknownFields); } @@ -3141,6 +3199,7 @@ namespace ScadaLink.Communication.Grpc { if (createdAtUtc_ != null) hash ^= CreatedAtUtc.GetHashCode(); if (updatedAtUtc_ != null) hash ^= UpdatedAtUtc.GetHashCode(); if (terminalAtUtc_ != null) hash ^= TerminalAtUtc.GetHashCode(); + if (SourceNode.Length != 0) hash ^= SourceNode.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -3202,6 +3261,10 @@ namespace ScadaLink.Communication.Grpc { output.WriteRawTag(90); output.WriteMessage(TerminalAtUtc); } + if (SourceNode.Length != 0) { + output.WriteRawTag(98); + output.WriteString(SourceNode); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -3255,6 +3318,10 @@ namespace ScadaLink.Communication.Grpc { output.WriteRawTag(90); output.WriteMessage(TerminalAtUtc); } + if (SourceNode.Length != 0) { + output.WriteRawTag(98); + output.WriteString(SourceNode); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -3298,6 +3365,9 @@ namespace ScadaLink.Communication.Grpc { if (terminalAtUtc_ != null) { size += 1 + pb::CodedOutputStream.ComputeMessageSize(TerminalAtUtc); } + if (SourceNode.Length != 0) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(SourceNode); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -3354,6 +3424,9 @@ namespace ScadaLink.Communication.Grpc { } TerminalAtUtc.MergeFrom(other.TerminalAtUtc); } + if (other.SourceNode.Length != 0) { + SourceNode = other.SourceNode; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -3429,6 +3502,10 @@ namespace ScadaLink.Communication.Grpc { input.ReadMessage(TerminalAtUtc); break; } + case 98: { + SourceNode = input.ReadString(); + break; + } } } #endif @@ -3504,6 +3581,10 @@ namespace ScadaLink.Communication.Grpc { input.ReadMessage(TerminalAtUtc); break; } + case 98: { + SourceNode = input.ReadString(); + break; + } } } } diff --git a/tests/ScadaLink.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs b/tests/ScadaLink.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs index 77c40aa..a18a2fc 100644 --- a/tests/ScadaLink.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs +++ b/tests/ScadaLink.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs @@ -100,6 +100,7 @@ public sealed class CombinedTelemetryDispatcher : ICachedCallTelemetryForwarder Channel = op.Channel, Target = op.Target, SourceSite = op.SourceSite, + SourceNode = op.SourceNode ?? string.Empty, Status = op.Status, RetryCount = op.RetryCount, LastError = op.LastError ?? string.Empty, diff --git a/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs b/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs index 06d1239..caf9e99 100644 --- a/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs +++ b/tests/ScadaLink.Communication.Tests/AuditEventDtoMapperTests.cs @@ -34,6 +34,7 @@ public class AuditEventDtoMapperTests ExecutionId = executionId, ParentExecutionId = parentExecutionId, SourceSiteId = "site-1", + SourceNode = "node-a", SourceInstanceId = "Pump01", SourceScript = "OnDemand", Actor = "design-key", @@ -61,6 +62,7 @@ public class AuditEventDtoMapperTests Assert.Equal(original.ExecutionId, roundTripped.ExecutionId); Assert.Equal(original.ParentExecutionId, roundTripped.ParentExecutionId); Assert.Equal(original.SourceSiteId, roundTripped.SourceSiteId); + Assert.Equal(original.SourceNode, roundTripped.SourceNode); Assert.Equal(original.SourceInstanceId, roundTripped.SourceInstanceId); Assert.Equal(original.SourceScript, roundTripped.SourceScript); Assert.Equal(original.Actor, roundTripped.Actor); @@ -99,6 +101,7 @@ public class AuditEventDtoMapperTests Assert.Equal(string.Empty, dto.ExecutionId); Assert.Equal(string.Empty, dto.ParentExecutionId); Assert.Equal(string.Empty, dto.SourceSiteId); + Assert.Equal(string.Empty, dto.SourceNode); Assert.Equal(string.Empty, dto.SourceInstanceId); Assert.Equal(string.Empty, dto.SourceScript); Assert.Equal(string.Empty, dto.Actor); @@ -124,6 +127,7 @@ public class AuditEventDtoMapperTests ExecutionId = string.Empty, ParentExecutionId = string.Empty, SourceSiteId = string.Empty, + SourceNode = string.Empty, SourceInstanceId = string.Empty, SourceScript = string.Empty, Actor = string.Empty, @@ -141,6 +145,7 @@ public class AuditEventDtoMapperTests Assert.Null(evt.ExecutionId); Assert.Null(evt.ParentExecutionId); Assert.Null(evt.SourceSiteId); + Assert.Null(evt.SourceNode); Assert.Null(evt.SourceInstanceId); Assert.Null(evt.SourceScript); Assert.Null(evt.Actor); @@ -232,4 +237,52 @@ public class AuditEventDtoMapperTests Assert.Equal("ApiCallCached", dto.Kind); Assert.Equal("Parked", dto.Status); } + + [Fact] + public void AuditEventDto_round_trip_preserves_SourceNode() + { + var evt = new AuditEvent + { + EventId = Guid.NewGuid(), + OccurredAtUtc = DateTime.UtcNow, + Channel = AuditChannel.ApiOutbound, + Kind = AuditKind.ApiCall, + Status = AuditStatus.Delivered, + SourceNode = "node-a" + }; + + var dto = AuditEventDtoMapper.ToDto(evt); + + // Wire form: empty-string-means-null convention; populated value + // travels verbatim. + Assert.Equal("node-a", dto.SourceNode); + + var roundTripped = AuditEventDtoMapper.FromDto(dto); + + Assert.Equal("node-a", roundTripped.SourceNode); + } + + [Fact] + public void AuditEventDto_round_trip_preserves_null_SourceNode() + { + var evt = new AuditEvent + { + EventId = Guid.NewGuid(), + OccurredAtUtc = DateTime.UtcNow, + Channel = AuditChannel.ApiOutbound, + Kind = AuditKind.ApiCall, + Status = AuditStatus.Delivered, + SourceNode = null + }; + + var dto = AuditEventDtoMapper.ToDto(evt); + + // ToDto collapses null → empty on the wire… + Assert.Equal(string.Empty, dto.SourceNode); + + var roundTripped = AuditEventDtoMapper.FromDto(dto); + + // …and FromDto rehydrates empty → null. + Assert.Null(roundTripped.SourceNode); + } } diff --git a/tests/ScadaLink.Communication.Tests/Protos/AuditEventProtoTests.cs b/tests/ScadaLink.Communication.Tests/Protos/AuditEventProtoTests.cs index 4cd0d48..632c8b5 100644 --- a/tests/ScadaLink.Communication.Tests/Protos/AuditEventProtoTests.cs +++ b/tests/ScadaLink.Communication.Tests/Protos/AuditEventProtoTests.cs @@ -25,6 +25,7 @@ public class AuditEventProtoTests Kind = "ApiCall", CorrelationId = Guid.NewGuid().ToString(), SourceSiteId = "site-1", + SourceNode = "node-a", SourceInstanceId = "Pump01", SourceScript = "OnDemand", Actor = "design-key", @@ -49,6 +50,7 @@ public class AuditEventProtoTests Assert.Equal(original.Kind, deserialized.Kind); Assert.Equal(original.CorrelationId, deserialized.CorrelationId); Assert.Equal(original.SourceSiteId, deserialized.SourceSiteId); + Assert.Equal(original.SourceNode, deserialized.SourceNode); Assert.Equal(original.SourceInstanceId, deserialized.SourceInstanceId); Assert.Equal(original.SourceScript, deserialized.SourceScript); Assert.Equal(original.Actor, deserialized.Actor); diff --git a/tests/ScadaLink.Communication.Tests/Protos/CachedTelemetryProtoTests.cs b/tests/ScadaLink.Communication.Tests/Protos/CachedTelemetryProtoTests.cs index 4405768..10d9e9b 100644 --- a/tests/ScadaLink.Communication.Tests/Protos/CachedTelemetryProtoTests.cs +++ b/tests/ScadaLink.Communication.Tests/Protos/CachedTelemetryProtoTests.cs @@ -39,6 +39,7 @@ public class CachedTelemetryProtoTests Channel = "ApiOutbound", Target = "ERP.GetOrder", SourceSite = "site-melbourne", + SourceNode = "node-a", Status = "Delivered", RetryCount = 3, LastError = "transient 503", @@ -55,6 +56,7 @@ public class CachedTelemetryProtoTests Assert.Equal(original.Channel, deserialized.Channel); Assert.Equal(original.Target, deserialized.Target); Assert.Equal(original.SourceSite, deserialized.SourceSite); + Assert.Equal(original.SourceNode, deserialized.SourceNode); Assert.Equal(original.Status, deserialized.Status); Assert.Equal(original.RetryCount, deserialized.RetryCount); Assert.Equal(original.LastError, deserialized.LastError); diff --git a/tests/ScadaLink.Communication.Tests/SiteCallDtoMapperTests.cs b/tests/ScadaLink.Communication.Tests/SiteCallDtoMapperTests.cs index 4de1d37..d9aa0bc 100644 --- a/tests/ScadaLink.Communication.Tests/SiteCallDtoMapperTests.cs +++ b/tests/ScadaLink.Communication.Tests/SiteCallDtoMapperTests.cs @@ -1,3 +1,4 @@ +using Google.Protobuf; using Google.Protobuf.WellKnownTypes; using ScadaLink.Communication.Grpc; @@ -28,6 +29,7 @@ public class SiteCallDtoMapperTests Channel = "ApiOutbound", Target = "ERP.GetOrder", SourceSite = "site-melbourne", + SourceNode = "node-a", Status = "Delivered", RetryCount = 3, LastError = "transient 503", @@ -43,6 +45,7 @@ public class SiteCallDtoMapperTests Assert.Equal("ApiOutbound", entity.Channel); Assert.Equal("ERP.GetOrder", entity.Target); Assert.Equal("site-melbourne", entity.SourceSite); + Assert.Equal("node-a", entity.SourceNode); Assert.Equal("Delivered", entity.Status); Assert.Equal(3, entity.RetryCount); Assert.Equal("transient 503", entity.LastError); @@ -121,6 +124,40 @@ public class SiteCallDtoMapperTests Assert.Throws(() => SiteCallDtoMapper.FromDto(null!)); } + [Fact] + public void SiteCallOperationalDto_round_trip_preserves_SourceNode() + { + // Populated SourceNode travels verbatim across the wire and through + // the DTO→entity mapper. + var dto = NewMinimalDto(); + dto.SourceNode = "node-a"; + + var bytes = dto.ToByteArray(); + var onWire = SiteCallOperationalDto.Parser.ParseFrom(bytes); + Assert.Equal("node-a", onWire.SourceNode); + + var entity = SiteCallDtoMapper.FromDto(onWire); + + Assert.Equal("node-a", entity.SourceNode); + } + + [Fact] + public void SiteCallOperationalDto_round_trip_preserves_null_SourceNode() + { + // The DTO uses an empty-string-means-null convention on the wire; + // FromDto rehydrates that back to a true null on the entity. + var dto = NewMinimalDto(); + // SourceNode left at proto default (empty string) — semantically null. + + var bytes = dto.ToByteArray(); + var onWire = SiteCallOperationalDto.Parser.ParseFrom(bytes); + Assert.Equal(string.Empty, onWire.SourceNode); + + var entity = SiteCallDtoMapper.FromDto(onWire); + + Assert.Null(entity.SourceNode); + } + private static SiteCallOperationalDto NewMinimalDto() => new() { TrackedOperationId = Guid.NewGuid().ToString(),