feat(notify): dual-write audit events to Timeline unified sink

Sprint SPRINT_20260408_005 DEPRECATE-001 (Notify, third service).

Same pattern as Authority + Policy dual-write: NotifyAuditRepository
now fans out to Timeline via the optional IAuditEventEmitter.
Fire-and-forget; local write stays authoritative.

Remaining DEPRECATE-001 services: Scheduler (ISchedulerAuditService),
JobEngine/ReleaseOrchestrator (PostgresAuditRepository.AppendAsync),
Attestor (audit log inserts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-19 22:39:14 +03:00
parent a7f3880e9f
commit 0acd2ecabb
2 changed files with 56 additions and 1 deletions

View File

@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using StellaOps.Audit.Emission;
using StellaOps.Notify.Persistence.EfCore.Context;
using StellaOps.Notify.Persistence.Postgres.Models;
@@ -10,11 +11,13 @@ public sealed class NotifyAuditRepository : INotifyAuditRepository
private const int CommandTimeoutSeconds = 30;
private readonly NotifyDataSource _dataSource;
private readonly ILogger<NotifyAuditRepository> _logger;
private readonly IAuditEventEmitter? _timelineEmitter;
public NotifyAuditRepository(NotifyDataSource dataSource, ILogger<NotifyAuditRepository> logger)
public NotifyAuditRepository(NotifyDataSource dataSource, ILogger<NotifyAuditRepository> logger, IAuditEventEmitter? timelineEmitter = null)
{
_dataSource = dataSource;
_logger = logger;
_timelineEmitter = timelineEmitter;
}
public async Task<long> CreateAsync(NotifyAuditEntity audit, CancellationToken cancellationToken = default)
@@ -23,9 +26,60 @@ public sealed class NotifyAuditRepository : INotifyAuditRepository
await using var dbContext = NotifyDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
dbContext.Audit.Add(audit);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
// DEPRECATE-001: dual-write to Timeline. Fire-and-forget; local write remains authoritative.
if (_timelineEmitter is not null)
{
try
{
await _timelineEmitter.EmitAsync(MapToTimelinePayload(audit), cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to emit notify audit event to Timeline (local write succeeded, id={AuditId})", audit.Id);
}
}
return audit.Id;
}
private static AuditEventPayload MapToTimelinePayload(NotifyAuditEntity audit)
{
var details = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["localAuditId"] = audit.Id
};
if (!string.IsNullOrWhiteSpace(audit.Details))
{
details["details"] = audit.Details;
}
return new AuditEventPayload
{
Id = $"notify-{audit.Id}",
Timestamp = audit.CreatedAt,
Module = "notify",
Action = audit.Action ?? "unknown",
Severity = "info",
Actor = new AuditActorPayload
{
Id = audit.UserId?.ToString() ?? "notify-system",
Name = audit.UserId?.ToString() ?? "notify-system",
Type = audit.UserId.HasValue ? "user" : "system"
},
Resource = new AuditResourcePayload
{
Type = audit.ResourceType ?? "notify_resource",
Id = audit.ResourceId ?? audit.Id.ToString(System.Globalization.CultureInfo.InvariantCulture)
},
Description = audit.Action ?? "notify audit event",
Details = details,
CorrelationId = audit.CorrelationId,
TenantId = audit.TenantId,
Tags = new[] { "notify", audit.Action ?? "unknown" }
};
}
public async Task<IReadOnlyList<NotifyAuditEntity>> ListAsync(string tenantId, int limit = 100, int offset = 0, CancellationToken cancellationToken = default)
{
await using var connection = await _dataSource.OpenConnectionAsync(tenantId, "reader", cancellationToken).ConfigureAwait(false);

View File

@@ -23,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Audit.Emission\StellaOps.Audit.Emission.csproj" />
<ProjectReference Include="..\StellaOps.Notify.Models\StellaOps.Notify.Models.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj" />