107 lines
3.5 KiB
C#
107 lines
3.5 KiB
C#
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<long> CommandCounter = Meter.CreateCounter<long>(
|
|
"tiku.database.commands",
|
|
description: "Number of database commands executed.");
|
|
internal static readonly Histogram<double> CommandDuration = Meter.CreateHistogram<double>(
|
|
"tiku.database.command.duration",
|
|
unit: "ms",
|
|
description: "Database command execution duration.");
|
|
internal static readonly Histogram<long> RequestCommandCount = Meter.CreateHistogram<long>(
|
|
"tiku.database.request.commands",
|
|
description: "Database commands executed during one HTTP request.");
|
|
internal static readonly Histogram<double> RequestCommandDuration = Meter.CreateHistogram<double>(
|
|
"tiku.database.request.duration",
|
|
unit: "ms",
|
|
description: "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<RequestState?> 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 long totalTicks;
|
|
private long slowestTicks;
|
|
private string? slowestFingerprint;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|