215 lines
6.8 KiB
C#
215 lines
6.8 KiB
C#
using System.Data.Common;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
|
|
namespace Jiaowu.Api.Infrastructure.Observability;
|
|
|
|
public sealed class DatabaseCommandTelemetryInterceptor(
|
|
ObservabilityOptions options,
|
|
IHttpContextAccessor httpContextAccessor,
|
|
ILogger<DatabaseCommandTelemetryInterceptor> logger)
|
|
: DbCommandInterceptor
|
|
{
|
|
public override DbDataReader ReaderExecuted(
|
|
DbCommand command,
|
|
CommandExecutedEventData eventData,
|
|
DbDataReader result)
|
|
{
|
|
Observe(command, eventData.Duration, "reader");
|
|
return result;
|
|
}
|
|
|
|
public override ValueTask<DbDataReader> ReaderExecutedAsync(
|
|
DbCommand command,
|
|
CommandExecutedEventData eventData,
|
|
DbDataReader result,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Observe(command, eventData.Duration, "reader");
|
|
return ValueTask.FromResult(result);
|
|
}
|
|
|
|
public override int NonQueryExecuted(
|
|
DbCommand command,
|
|
CommandExecutedEventData eventData,
|
|
int result)
|
|
{
|
|
Observe(command, eventData.Duration, "nonquery");
|
|
return result;
|
|
}
|
|
|
|
public override ValueTask<int> NonQueryExecutedAsync(
|
|
DbCommand command,
|
|
CommandExecutedEventData eventData,
|
|
int result,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Observe(command, eventData.Duration, "nonquery");
|
|
return ValueTask.FromResult(result);
|
|
}
|
|
|
|
public override object? ScalarExecuted(
|
|
DbCommand command,
|
|
CommandExecutedEventData eventData,
|
|
object? result)
|
|
{
|
|
Observe(command, eventData.Duration, "scalar");
|
|
return result;
|
|
}
|
|
|
|
public override ValueTask<object?> ScalarExecutedAsync(
|
|
DbCommand command,
|
|
CommandExecutedEventData eventData,
|
|
object? result,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Observe(command, eventData.Duration, "scalar");
|
|
return ValueTask.FromResult(result);
|
|
}
|
|
|
|
public override void CommandFailed(
|
|
DbCommand command,
|
|
CommandErrorEventData eventData) =>
|
|
Observe(
|
|
command,
|
|
eventData.Duration,
|
|
"failed",
|
|
eventData.Exception.GetType().Name);
|
|
|
|
public override Task CommandFailedAsync(
|
|
DbCommand command,
|
|
CommandErrorEventData eventData,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Observe(
|
|
command,
|
|
eventData.Duration,
|
|
"failed",
|
|
eventData.Exception.GetType().Name);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public override void CommandCanceled(
|
|
DbCommand command,
|
|
CommandEndEventData eventData) =>
|
|
Observe(command, eventData.Duration, "canceled", "canceled");
|
|
|
|
public override Task CommandCanceledAsync(
|
|
DbCommand command,
|
|
CommandEndEventData eventData,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
Observe(command, eventData.Duration, "canceled", "canceled");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private void Observe(
|
|
DbCommand command,
|
|
TimeSpan duration,
|
|
string commandKind,
|
|
string? errorType = null)
|
|
{
|
|
if (!options.Enabled) return;
|
|
|
|
var queryName = GetQueryName(command.CommandText);
|
|
var statementHash = GetStatementHash(command.CommandText);
|
|
var provider = GetProviderName(command);
|
|
var requestId = httpContextAccessor.HttpContext?.TraceIdentifier ?? "background";
|
|
var durationMilliseconds = duration.TotalMilliseconds;
|
|
|
|
if (errorType is not null)
|
|
{
|
|
logger.LogError(
|
|
"Database command failed after {DurationMs:F1} ms: " +
|
|
"{QueryName} ({CommandKind}, {Provider}, hash {StatementHash}, " +
|
|
"error {ErrorType}, request {RequestId}).",
|
|
durationMilliseconds,
|
|
queryName,
|
|
commandKind,
|
|
provider,
|
|
statementHash,
|
|
errorType,
|
|
requestId);
|
|
return;
|
|
}
|
|
|
|
if (durationMilliseconds < options.SlowQueryThresholdMilliseconds)
|
|
return;
|
|
|
|
if (options.IncludeSqlText)
|
|
{
|
|
logger.LogWarning(
|
|
"Slow database command took {DurationMs:F1} ms: " +
|
|
"{QueryName} ({CommandKind}, {Provider}, hash {StatementHash}, " +
|
|
"request {RequestId}). " +
|
|
"SQL template: {SqlTemplate}",
|
|
durationMilliseconds,
|
|
queryName,
|
|
commandKind,
|
|
provider,
|
|
statementHash,
|
|
requestId,
|
|
Truncate(command.CommandText, options.MaximumSqlTextLength));
|
|
}
|
|
else
|
|
{
|
|
logger.LogWarning(
|
|
"Slow database command took {DurationMs:F1} ms: " +
|
|
"{QueryName} ({CommandKind}, {Provider}, hash {StatementHash}, " +
|
|
"request {RequestId}).",
|
|
durationMilliseconds,
|
|
queryName,
|
|
commandKind,
|
|
provider,
|
|
statementHash,
|
|
requestId);
|
|
}
|
|
}
|
|
|
|
internal static string GetQueryName(string commandText)
|
|
{
|
|
using var reader = new StringReader(commandText);
|
|
while (reader.ReadLine() is { } line)
|
|
{
|
|
var trimmed = line.Trim();
|
|
if (trimmed.Length == 0) continue;
|
|
if (trimmed.StartsWith("-- ", StringComparison.Ordinal))
|
|
return Truncate(trimmed[3..].Trim(), 120);
|
|
return $"{FirstToken(trimmed)}:{GetStatementHash(commandText)}";
|
|
}
|
|
|
|
return $"unknown:{GetStatementHash(commandText)}";
|
|
}
|
|
|
|
internal static string GetStatementHash(string commandText)
|
|
{
|
|
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(commandText));
|
|
return Convert.ToHexString(bytes.AsSpan(0, 6)).ToLowerInvariant();
|
|
}
|
|
|
|
private static string FirstToken(string value)
|
|
{
|
|
var end = value.IndexOfAny([' ', '\t', '\r', '\n', '(']);
|
|
var token = end < 0 ? value : value[..end];
|
|
return token.Length == 0
|
|
? "command"
|
|
: token.ToLowerInvariant();
|
|
}
|
|
|
|
private static string GetProviderName(DbCommand command)
|
|
{
|
|
var typeName = command.GetType().FullName ?? command.GetType().Name;
|
|
if (typeName.Contains("MySql", StringComparison.OrdinalIgnoreCase))
|
|
return "mysql";
|
|
if (typeName.Contains("Sqlite", StringComparison.OrdinalIgnoreCase))
|
|
return "sqlite";
|
|
return "other_sql";
|
|
}
|
|
|
|
private static string Truncate(string value, int maximumLength) =>
|
|
value.Length <= maximumLength
|
|
? value
|
|
: value[..maximumLength];
|
|
}
|