feat(server): add observability and operator controls (#16)
quality-gate / quality (push) Failing after 1m1s

This commit is contained in:
KyuubiYoru
2026-07-16 13:22:16 +02:00
parent 88ef946af5
commit be732de7c9
31 changed files with 4149 additions and 140 deletions
@@ -0,0 +1,134 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
namespace FinalFactory.Rendezvous.Server.Observability;
internal sealed partial class AuditTrail
{
private readonly object _gate = new();
private readonly LinkedList<AuditEntry> _entries = [];
private readonly AuditOptions _options;
private readonly TimeProvider _timeProvider;
private readonly ILogger<AuditTrail> _logger;
private readonly RendezvousTelemetry _telemetry;
public AuditTrail(
IOptions<AuditOptions> options,
ILogger<AuditTrail> logger,
RendezvousTelemetry telemetry,
TimeProvider? timeProvider = null)
{
_options = options.Value;
_logger = logger;
_telemetry = telemetry;
_timeProvider = timeProvider ?? TimeProvider.System;
}
public void Record(
string actorSubject,
string action,
string result,
string targetKind,
string targetIdentifier,
string correlationId)
{
DateTimeOffset now = _timeProvider.GetUtcNow();
AuditEntry entry = new(
now,
Fingerprint(actorSubject),
action,
result,
targetKind,
Fingerprint(targetIdentifier),
correlationId);
lock (_gate)
{
PurgeExpired(now);
while (_entries.Count >= _options.MaxEntries)
{
_entries.RemoveFirst();
}
_entries.AddLast(entry);
}
_telemetry.RecordAudit(action, result);
LogOperatorAction(
_logger,
entry.Timestamp,
entry.ActorFingerprint,
action,
result,
targetKind,
entry.TargetFingerprint,
correlationId);
}
public IReadOnlyDictionary<string, long> GetAggregateCounts()
{
lock (_gate)
{
PurgeExpired(_timeProvider.GetUtcNow());
return _entries
.GroupBy(static entry => $"{entry.Action}:{entry.Result}", StringComparer.Ordinal)
.ToDictionary(
static group => group.Key,
static group => (long)group.Count(),
StringComparer.Ordinal);
}
}
internal IReadOnlyList<AuditEntry> GetEntriesForTests()
{
lock (_gate)
{
PurgeExpired(_timeProvider.GetUtcNow());
return _entries.ToArray();
}
}
private void PurgeExpired(DateTimeOffset now)
{
DateTimeOffset oldest = now.AddDays(-_options.RetentionDays);
while (_entries.First is { Value.Timestamp: var timestamp }
&& timestamp < oldest)
{
_entries.RemoveFirst();
}
}
private static string Fingerprint(string value)
{
byte[] digest = SHA256.HashData(Encoding.UTF8.GetBytes(value));
return Convert.ToHexString(digest.AsSpan(0, 12));
}
[LoggerMessage(
EventId = 100,
Level = LogLevel.Information,
Message = "Operator audit at {Timestamp}: actor {ActorFingerprint} action {Action} completed with {Result} for {TargetKind} target {TargetFingerprint}; correlation {CorrelationId}")]
private static partial void LogOperatorAction(
ILogger logger,
DateTimeOffset timestamp,
string actorFingerprint,
string action,
string result,
string targetKind,
string targetFingerprint,
string correlationId);
}
internal sealed record AuditEntry(
DateTimeOffset Timestamp,
string ActorFingerprint,
string Action,
string Result,
string TargetKind,
string TargetFingerprint,
string CorrelationId)
{
public override string ToString() =>
$"[AuditEntry {Action}/{Result}; actor and target fingerprinted]";
}