using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; namespace ScadaLink.AuditLog.Central; /// /// Central-only direct-write implementation of . /// Wraps as a best-effort /// audit emission path for components that originate audit events ON the central /// node (Notification Outbox dispatch, Inbound API) — NOT for site telemetry /// ingest (that path is the SiteAudit → AuditLogIngestActor batched flow). /// /// /// /// Best-effort contract. Audit-write failures NEVER abort the user-facing /// action (alog.md §13). The writer catches every exception thrown by repository /// resolution or the insert call, logs at warning, and returns successfully. /// Callers may still wrap the call in their own try/catch (defensive — the writer /// is supposed to swallow). /// /// /// Scope-per-call resolution. is a SCOPED /// EF Core service (registered by ScadaLink.ConfigurationDatabase). The /// writer itself is registered as a singleton (so all callers share one instance), /// so it cannot hold a scope across calls — it opens a fresh /// per invocation, mirroring /// the per-message scope pattern used by AuditLogIngestActor and /// NotificationOutboxActor. /// /// /// Idempotency. Persistence is via InsertIfNotExistsAsync, so a /// double-emitted event (same ) is a silent /// no-op — the writer is safe to call from any number of dispatch paths. /// /// public sealed class CentralAuditWriter : ICentralAuditWriter { private readonly IServiceProvider _services; private readonly ILogger _logger; public CentralAuditWriter(IServiceProvider services, ILogger logger) { _services = services ?? throw new ArgumentNullException(nameof(services)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// /// Persists into the central AuditLog table /// idempotently on . Stamps /// from the central-side clock. /// Internal failures are logged and swallowed — never thrown. /// public async Task WriteAsync(AuditEvent evt, CancellationToken ct = default) { if (evt is null) { // Defensive — a null event is a programming bug at the caller and // produces no meaningful audit row. Log and return. _logger.LogWarning("CentralAuditWriter.WriteAsync received null event; ignoring."); return; } try { await using var scope = _services.CreateAsyncScope(); var repo = scope.ServiceProvider.GetRequiredService(); var stamped = evt with { IngestedAtUtc = DateTime.UtcNow }; await repo.InsertIfNotExistsAsync(stamped, ct).ConfigureAwait(false); } catch (Exception ex) { // Audit failure NEVER aborts the user-facing action — swallow and log. _logger.LogWarning( ex, "CentralAuditWriter failed for EventId {EventId} (Kind={Kind}, Status={Status})", evt.EventId, evt.Kind, evt.Status); } } }