refactor(auditlog): consolidate AuditEvent DTO mappers into Communication

This commit is contained in:
Joseph Doherty
2026-05-21 03:51:51 -04:00
parent 6f59a1b546
commit fdd1a4b886
9 changed files with 45 additions and 153 deletions

View File

@@ -1,5 +1,4 @@
using Akka.Actor;
using ScadaLink.AuditLog.Telemetry;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Messages.Audit;
using ScadaLink.Commons.Types;
@@ -35,7 +34,7 @@ namespace ScadaLink.AuditLog.Site.Telemetry;
/// The batches arrive as proto DTOs (<see cref="AuditEventBatch"/> /
/// <see cref="CachedTelemetryBatch"/>) because the
/// <see cref="SiteAuditTelemetryActor"/> builds them with
/// <see cref="AuditEventMapper.ToDto"/>. This client converts them back into
/// <see cref="AuditEventDtoMapper.ToDto"/>. This client converts them back into
/// the <see cref="AuditEvent"/> / <see cref="SiteCall"/> entities the Akka
/// command messages carry — the same DTO→entity translation the
/// <c>SiteStreamGrpcServer</c> performs for the gRPC reconciliation path.
@@ -71,7 +70,7 @@ public sealed class ClusterClientSiteAuditClient : ISiteStreamAuditClient
var events = new List<AuditEvent>(batch.Events.Count);
foreach (var dto in batch.Events)
{
events.Add(AuditEventMapper.FromDto(dto));
events.Add(AuditEventDtoMapper.FromDto(dto));
}
// Ask<T> throws AskTimeoutException on timeout and rethrows a
@@ -92,7 +91,7 @@ public sealed class ClusterClientSiteAuditClient : ISiteStreamAuditClient
var entries = new List<CachedTelemetryEntry>(batch.Packets.Count);
foreach (var packet in batch.Packets)
{
var audit = AuditEventMapper.FromDto(packet.AuditEvent);
var audit = AuditEventDtoMapper.FromDto(packet.AuditEvent);
var siteCall = MapSiteCall(packet.Operational);
entries.Add(new CachedTelemetryEntry(audit, siteCall));
}

View File

@@ -1,7 +1,6 @@
using Akka.Actor;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ScadaLink.AuditLog.Telemetry;
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Communication.Grpc;
@@ -136,7 +135,7 @@ public class SiteAuditTelemetryActor : ReceiveActor
var batch = new AuditEventBatch();
foreach (var e in events)
{
batch.Events.Add(AuditEventMapper.ToDto(e));
batch.Events.Add(AuditEventDtoMapper.ToDto(e));
}
return batch;
}

View File

@@ -1,112 +0,0 @@
using ScadaLink.Commons.Entities.Audit;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Communication.Grpc;
using Timestamp = Google.Protobuf.WellKnownTypes.Timestamp;
namespace ScadaLink.AuditLog.Telemetry;
/// <summary>
/// Bridges Audit Log (#23) rows between the in-process <see cref="AuditEvent"/> record
/// and the wire-format <see cref="AuditEventDto"/> exchanged over the
/// <c>IngestAuditEvents</c> RPC.
/// </summary>
/// <remarks>
/// <para><b>Lossy by design:</b> the proto contract intentionally omits two fields.</para>
/// <list type="bullet">
/// <item><see cref="AuditEvent.ForwardState"/> — site-local SQLite state, never travels.</item>
/// <item><see cref="AuditEvent.IngestedAtUtc"/> — central-set at ingest time, not at the site.</item>
/// </list>
/// <para>
/// String nullability convention: proto3 scalar strings cannot be absent, so nullable
/// .NET strings round-trip as empty strings on the wire. Nullable integers use the
/// <c>Int32Value</c> wrapper so they preserve true null semantics.
/// </para>
/// </remarks>
public static class AuditEventMapper
{
/// <summary>
/// Projects an <see cref="AuditEvent"/> into its wire-format DTO. Null reference
/// fields collapse to empty strings; null integer fields leave the wrapper unset.
/// </summary>
public static AuditEventDto ToDto(AuditEvent evt)
{
ArgumentNullException.ThrowIfNull(evt);
var dto = new AuditEventDto
{
EventId = evt.EventId.ToString(),
OccurredAtUtc = Timestamp.FromDateTime(EnsureUtc(evt.OccurredAtUtc)),
Channel = evt.Channel.ToString(),
Kind = evt.Kind.ToString(),
CorrelationId = evt.CorrelationId?.ToString() ?? string.Empty,
SourceSiteId = evt.SourceSiteId ?? string.Empty,
SourceInstanceId = evt.SourceInstanceId ?? string.Empty,
SourceScript = evt.SourceScript ?? string.Empty,
Actor = evt.Actor ?? string.Empty,
Target = evt.Target ?? string.Empty,
Status = evt.Status.ToString(),
ErrorMessage = evt.ErrorMessage ?? string.Empty,
ErrorDetail = evt.ErrorDetail ?? string.Empty,
RequestSummary = evt.RequestSummary ?? string.Empty,
ResponseSummary = evt.ResponseSummary ?? string.Empty,
PayloadTruncated = evt.PayloadTruncated,
Extra = evt.Extra ?? string.Empty
};
if (evt.HttpStatus.HasValue)
{
dto.HttpStatus = evt.HttpStatus.Value;
}
if (evt.DurationMs.HasValue)
{
dto.DurationMs = evt.DurationMs.Value;
}
return dto;
}
/// <summary>
/// Reconstructs an <see cref="AuditEvent"/> from its wire-format DTO. Empty strings
/// rehydrate as null reference values; absent integer wrappers stay null.
/// <see cref="AuditEvent.ForwardState"/> and <see cref="AuditEvent.IngestedAtUtc"/>
/// are intentionally left null — the central ingest actor sets the latter.
/// </summary>
public static AuditEvent FromDto(AuditEventDto dto)
{
ArgumentNullException.ThrowIfNull(dto);
return new AuditEvent
{
EventId = Guid.Parse(dto.EventId),
OccurredAtUtc = DateTime.SpecifyKind(dto.OccurredAtUtc.ToDateTime(), DateTimeKind.Utc),
IngestedAtUtc = null,
Channel = Enum.Parse<AuditChannel>(dto.Channel),
Kind = Enum.Parse<AuditKind>(dto.Kind),
CorrelationId = NullIfEmpty(dto.CorrelationId) is { } cid ? Guid.Parse(cid) : null,
SourceSiteId = NullIfEmpty(dto.SourceSiteId),
SourceInstanceId = NullIfEmpty(dto.SourceInstanceId),
SourceScript = NullIfEmpty(dto.SourceScript),
Actor = NullIfEmpty(dto.Actor),
Target = NullIfEmpty(dto.Target),
Status = Enum.Parse<AuditStatus>(dto.Status),
HttpStatus = dto.HttpStatus,
DurationMs = dto.DurationMs,
ErrorMessage = NullIfEmpty(dto.ErrorMessage),
ErrorDetail = NullIfEmpty(dto.ErrorDetail),
RequestSummary = NullIfEmpty(dto.RequestSummary),
ResponseSummary = NullIfEmpty(dto.ResponseSummary),
PayloadTruncated = dto.PayloadTruncated,
Extra = NullIfEmpty(dto.Extra),
ForwardState = null
};
}
private static string? NullIfEmpty(string? value) =>
string.IsNullOrEmpty(value) ? null : value;
private static DateTime EnsureUtc(DateTime value) =>
value.Kind == DateTimeKind.Utc
? value
: DateTime.SpecifyKind(value.ToUniversalTime(), DateTimeKind.Utc);
}