forked from gongxuegit/tiku-backend.net
perf: optimize authorization scoreline and workers
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Tiku.Infrastructure.Observability;
|
||||
|
||||
public sealed class DatabasePerformanceInterceptor(
|
||||
ILogger<DatabasePerformanceInterceptor> logger) : DbCommandInterceptor
|
||||
{
|
||||
private const double SlowCommandMilliseconds = 500;
|
||||
|
||||
public override DbDataReader ReaderExecuted(
|
||||
DbCommand command,
|
||||
CommandExecutedEventData eventData,
|
||||
DbDataReader result)
|
||||
{
|
||||
Record(command, eventData.Duration, "reader");
|
||||
return result;
|
||||
}
|
||||
|
||||
public override ValueTask<DbDataReader> ReaderExecutedAsync(
|
||||
DbCommand command,
|
||||
CommandExecutedEventData eventData,
|
||||
DbDataReader result,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Record(command, eventData.Duration, "reader");
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
|
||||
public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result)
|
||||
{
|
||||
Record(command, eventData.Duration, "non_query");
|
||||
return result;
|
||||
}
|
||||
|
||||
public override ValueTask<int> NonQueryExecutedAsync(
|
||||
DbCommand command,
|
||||
CommandExecutedEventData eventData,
|
||||
int result,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Record(command, eventData.Duration, "non_query");
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
|
||||
public override object? ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object? result)
|
||||
{
|
||||
Record(command, eventData.Duration, "scalar");
|
||||
return result;
|
||||
}
|
||||
|
||||
public override ValueTask<object?> ScalarExecutedAsync(
|
||||
DbCommand command,
|
||||
CommandExecutedEventData eventData,
|
||||
object? result,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
Record(command, eventData.Duration, "scalar");
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
|
||||
private void Record(DbCommand command, TimeSpan duration, string operation)
|
||||
{
|
||||
var fingerprint = DatabasePerformanceTelemetry.Fingerprint(command.CommandText);
|
||||
var tags = new TagList { { "db.operation", operation } };
|
||||
DatabasePerformanceTelemetry.CommandCounter.Add(1, tags);
|
||||
DatabasePerformanceTelemetry.CommandDuration.Record(
|
||||
duration.TotalMilliseconds,
|
||||
tags);
|
||||
DatabaseRequestMetrics.Record(duration, fingerprint);
|
||||
|
||||
if (duration.TotalMilliseconds >= SlowCommandMilliseconds)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Slow database command {CommandFingerprint} ({Operation}) completed in {ElapsedMilliseconds:F1} ms.",
|
||||
fingerprint,
|
||||
operation,
|
||||
duration.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Tiku.Infrastructure/Observability/DatabaseRequestMetrics.cs
Normal file
106
Tiku.Infrastructure/Observability/DatabaseRequestMetrics.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user