Add audit logs.
ci-cd-vm / ci-cd (push) Successful in 4m2s

This commit is contained in:
Chris Chen
2026-06-23 12:13:47 -07:00
parent 870eeec82a
commit 62592c29ae
106 changed files with 2522 additions and 311 deletions
@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Entities.Logging;
namespace ROLAC.API.Data.Logging;
/// <summary>
/// A minimal, write-mostly context dedicated to the SystemLog / AuditLog tables. It is the
/// structural break that prevents log-storms: it is registered WITHOUT the audit interceptors
/// and with a silent logger factory (see Program.cs), so persisting a log row produces no log
/// events that the DB sink would pick up. It shares the same physical database/connection as
/// AppDbContext, but the tables themselves are created by AppDbContext's migration — they are
/// only mapped here so this context can read/write them.
/// </summary>
public class LogDbContext : DbContext
{
public LogDbContext(DbContextOptions<LogDbContext> options) : base(options) { }
public DbSet<SystemLog> SystemLogs => Set<SystemLog>();
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
LogModelConfiguration.Configure(builder);
}
}
@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Entities.Logging;
namespace ROLAC.API.Data.Logging;
/// <summary>
/// Single source of truth for the SystemLog / AuditLog table schema. Applied by
/// <see cref="AppDbContext"/> (so the startup migration creates the tables) AND by
/// <see cref="LogDbContext"/> (so runtime reads/writes map to the same shape).
/// </summary>
public static class LogModelConfiguration
{
public static void Configure(ModelBuilder builder)
{
builder.Entity<SystemLog>(entity =>
{
entity.ToTable("SystemLogs");
entity.HasKey(e => e.Id);
entity.Property(e => e.Level).HasConversion<byte>();
entity.Property(e => e.Category).HasMaxLength(256).IsRequired();
entity.Property(e => e.Message).IsRequired(); // text
entity.Property(e => e.RequestPath).HasMaxLength(2048);
entity.Property(e => e.HttpMethod).HasMaxLength(10);
entity.Property(e => e.UserId).HasMaxLength(450);
entity.Property(e => e.IpAddress).HasMaxLength(45);
entity.Property(e => e.CorrelationId).HasMaxLength(64);
entity.HasIndex(e => e.Timestamp);
entity.HasIndex(e => e.Level);
entity.HasIndex(e => new { e.Timestamp, e.Level });
entity.HasIndex(e => e.UserId).HasFilter("\"UserId\" IS NOT NULL");
});
builder.Entity<AuditLog>(entity =>
{
entity.ToTable("AuditLogs");
entity.HasKey(e => e.Id);
entity.Property(e => e.Level).HasConversion<byte>();
entity.Property(e => e.Action).HasMaxLength(40).IsRequired();
entity.Property(e => e.Category).HasMaxLength(40).IsRequired();
entity.Property(e => e.EntityName).HasMaxLength(128);
entity.Property(e => e.EntityId).HasMaxLength(64);
entity.Property(e => e.Changes).HasColumnType("jsonb");
entity.Property(e => e.Summary).HasMaxLength(512);
entity.Property(e => e.UserId).HasMaxLength(450);
entity.Property(e => e.UserEmail).HasMaxLength(256);
entity.Property(e => e.IpAddress).HasMaxLength(45);
entity.Property(e => e.CorrelationId).HasMaxLength(64);
entity.HasIndex(e => e.Timestamp);
entity.HasIndex(e => new { e.Category, e.Timestamp });
entity.HasIndex(e => new { e.EntityName, e.EntityId });
entity.HasIndex(e => e.Action);
entity.HasIndex(e => e.UserId).HasFilter("\"UserId\" IS NOT NULL");
});
}
}