using System.Diagnostics; using System.Diagnostics.Metrics; using System.Security.Cryptography; using System.Text; namespace Tiku.Infrastructure.Observability; public sealed record DatabaseRequestSnapshot( int CommandCount, double TotalDurationMilliseconds, double SlowestDurationMilliseconds, string? SlowestCommandFingerprint); public static class DatabasePerformanceTelemetry { public const string MeterName = "Tiku.Database"; internal static readonly Meter Meter = new(MeterName, "1.0.0"); internal static readonly Counter CommandCounter = Meter.CreateCounter( "tiku.database.commands", description: "Number of database commands executed."); internal static readonly Histogram CommandDuration = Meter.CreateHistogram( "tiku.database.command.duration", "ms", "Database command execution duration."); internal static readonly Histogram RequestCommandCount = Meter.CreateHistogram( "tiku.database.request.commands", description: "Database commands executed during one HTTP request."); internal static readonly Histogram RequestCommandDuration = Meter.CreateHistogram( "tiku.database.request.duration", "ms", "Aggregate database command duration during one HTTP request."); public static string Fingerprint(string commandText) { var hash = SHA256.HashData(Encoding.UTF8.GetBytes(commandText)); return Convert.ToHexString(hash.AsSpan(0, 8)).ToLowerInvariant(); } } public static class DatabaseRequestMetrics { private static readonly AsyncLocal CurrentState = new(); public static IDisposable Begin() { var previous = CurrentState.Value; var current = new RequestState(); CurrentState.Value = current; return new Scope(previous, current); } internal static void Record(TimeSpan duration, string fingerprint) { CurrentState.Value?.Record(duration, fingerprint); } private sealed class RequestState { private readonly Stopwatch stopwatch = Stopwatch.StartNew(); private int commandCount; private string? slowestFingerprint; private long slowestTicks; private long totalTicks; public void Record(TimeSpan duration, string fingerprint) { commandCount++; totalTicks += duration.Ticks; if (duration.Ticks > slowestTicks) { slowestTicks = duration.Ticks; slowestFingerprint = fingerprint; } } public DatabaseRequestSnapshot Complete() { stopwatch.Stop(); return new DatabaseRequestSnapshot( commandCount, TimeSpan.FromTicks(totalTicks).TotalMilliseconds, TimeSpan.FromTicks(slowestTicks).TotalMilliseconds, slowestFingerprint); } } private sealed class Scope(RequestState? previous, RequestState current) : IDisposable { private bool disposed; public void Dispose() { if (disposed) return; disposed = true; var snapshot = current.Complete(); DatabasePerformanceTelemetry.RequestCommandCount.Record(snapshot.CommandCount); DatabasePerformanceTelemetry.RequestCommandDuration.Record(snapshot.TotalDurationMilliseconds); CurrentState.Value = previous; } } }