feat(auditlog): ExecutionId column on AuditEvent + central AuditLog

This commit is contained in:
Joseph Doherty
2026-05-21 14:43:35 -04:00
parent 4002f4197b
commit fd12021984
9 changed files with 1754 additions and 7 deletions

View File

@@ -89,6 +89,10 @@ public class AuditLogEntityTypeConfiguration : IEntityTypeConfiguration<AuditEve
.HasFilter("[CorrelationId] IS NOT NULL")
.HasDatabaseName("IX_AuditLog_CorrelationId");
builder.HasIndex(e => e.ExecutionId)
.HasFilter("[ExecutionId] IS NOT NULL")
.HasDatabaseName("IX_AuditLog_Execution");
builder.HasIndex(e => new { e.Channel, e.Status, e.OccurredAtUtc })
.IsDescending(false, false, true)
.HasDatabaseName("IX_AuditLog_Channel_Status_Occurred");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ScadaLink.ConfigurationDatabase.Migrations
{
/// <summary>
/// Adds the universal <c>ExecutionId</c> correlation column to the centralized
/// <c>AuditLog</c> table (#23). <c>ExecutionId</c> identifies the originating
/// script execution / inbound request and is distinct from the per-operation
/// <c>CorrelationId</c>.
///
/// The change is purely additive:
/// 1. <c>ExecutionId uniqueidentifier NULL</c> is added with no default, so the
/// operation is a metadata-only <c>ALTER TABLE … ADD</c> — it does NOT
/// rewrite the monthly-partitioned <c>AuditLog</c> table, and historical
/// rows stay <c>NULL</c> (no backfill).
/// 2. <c>IX_AuditLog_Execution</c> is created via raw SQL so it lands on the
/// <c>ps_AuditLog_Month(OccurredAtUtc)</c> partition scheme, matching every
/// other <c>IX_AuditLog_*</c> index. Keeping it partition-aligned preserves
/// the partition-switch purge path (see AuditLogRepository.SwitchOutPartitionAsync).
/// </summary>
public partial class AddAuditLogExecutionId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ExecutionId",
table: "AuditLog",
type: "uniqueidentifier",
nullable: true);
// Raw SQL so the index is created on the partition scheme — EF's
// CreateIndex cannot express the ON ps_AuditLog_Month(OccurredAtUtc)
// clause. Mirrors IX_AuditLog_CorrelationId (filtered, aligned).
migrationBuilder.Sql(@"
CREATE NONCLUSTERED INDEX IX_AuditLog_Execution
ON dbo.AuditLog (ExecutionId)
WHERE ExecutionId IS NOT NULL
ON ps_AuditLog_Month(OccurredAtUtc);");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
IF EXISTS (SELECT 1 FROM sys.indexes WHERE name = 'IX_AuditLog_Execution' AND object_id = OBJECT_ID('dbo.AuditLog'))
DROP INDEX IX_AuditLog_Execution ON dbo.AuditLog;");
migrationBuilder.DropColumn(
name: "ExecutionId",
table: "AuditLog");
}
}
}

View File

@@ -73,6 +73,9 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<Guid?>("ExecutionId")
.HasColumnType("uniqueidentifier");
b.Property<string>("Extra")
.HasColumnType("nvarchar(max)");
@@ -138,6 +141,10 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
.IsUnique()
.HasDatabaseName("UX_AuditLog_EventId");
b.HasIndex("ExecutionId")
.HasDatabaseName("IX_AuditLog_Execution")
.HasFilter("[ExecutionId] IS NOT NULL");
b.HasIndex("OccurredAtUtc")
.IsDescending()
.HasDatabaseName("IX_AuditLog_OccurredAtUtc");

View File

@@ -64,12 +64,12 @@ public class AuditLogRepository : IAuditLogRepository
await _context.Database.ExecuteSqlInterpolatedAsync(
$@"IF NOT EXISTS (SELECT 1 FROM dbo.AuditLog WHERE EventId = {evt.EventId})
INSERT INTO dbo.AuditLog
(EventId, OccurredAtUtc, IngestedAtUtc, Channel, Kind, CorrelationId,
(EventId, OccurredAtUtc, IngestedAtUtc, Channel, Kind, CorrelationId, ExecutionId,
SourceSiteId, SourceInstanceId, SourceScript, Actor, Target, Status,
HttpStatus, DurationMs, ErrorMessage, ErrorDetail, RequestSummary,
ResponseSummary, PayloadTruncated, Extra, ForwardState)
VALUES
({evt.EventId}, {evt.OccurredAtUtc}, {evt.IngestedAtUtc}, {channel}, {kind}, {evt.CorrelationId},
({evt.EventId}, {evt.OccurredAtUtc}, {evt.IngestedAtUtc}, {channel}, {kind}, {evt.CorrelationId}, {evt.ExecutionId},
{evt.SourceSiteId}, {evt.SourceInstanceId}, {evt.SourceScript}, {evt.Actor}, {evt.Target}, {status},
{evt.HttpStatus}, {evt.DurationMs}, {evt.ErrorMessage}, {evt.ErrorDetail}, {evt.RequestSummary},
{evt.ResponseSummary}, {evt.PayloadTruncated}, {evt.Extra}, {forwardState});",
@@ -157,6 +157,11 @@ VALUES
query = query.Where(e => e.CorrelationId == correlationId);
}
if (filter.ExecutionId is { } executionId)
{
query = query.Where(e => e.ExecutionId == executionId);
}
if (filter.FromUtc is { } fromUtc)
{
query = query.Where(e => e.OccurredAtUtc >= fromUtc);
@@ -263,6 +268,7 @@ VALUES
PayloadTruncated bit NOT NULL,
Extra nvarchar(max) NULL,
ForwardState varchar(32) NULL,
ExecutionId uniqueidentifier NULL,
CONSTRAINT PK_{stagingTableName} PRIMARY KEY CLUSTERED (EventId, OccurredAtUtc)
) ON [PRIMARY];