弃用 OpenTelemetry,并建立日志驱动的真实慢接口基线
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics;
|
||||
using Jiaowu.Api.Infrastructure.Observability;
|
||||
|
||||
namespace Jiaowu.Api.Infrastructure.Middleware;
|
||||
|
||||
public sealed class SlowRequestLoggingMiddleware(
|
||||
RequestDelegate next,
|
||||
ObservabilityOptions options,
|
||||
ILogger<SlowRequestLoggingMiddleware> logger)
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
if (!options.Enabled ||
|
||||
!context.Request.Path.StartsWithSegments("/api"))
|
||||
{
|
||||
await next(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var startedAt = Stopwatch.GetTimestamp();
|
||||
await next(context);
|
||||
|
||||
var durationMilliseconds = Stopwatch.GetElapsedTime(startedAt).TotalMilliseconds;
|
||||
var endpoint = context.GetEndpoint()?.DisplayName ?? context.Request.Path.Value ?? "/api";
|
||||
if (durationMilliseconds >= options.SlowRequestThresholdMilliseconds ||
|
||||
context.Response.StatusCode >= StatusCodes.Status500InternalServerError)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Slow API request completed: {Method} {Path} ({Endpoint}) returned {StatusCode} in {DurationMs:F1} ms, request {RequestId}.",
|
||||
context.Request.Method,
|
||||
context.Request.Path.Value,
|
||||
endpoint,
|
||||
context.Response.StatusCode,
|
||||
durationMilliseconds,
|
||||
context.TraceIdentifier);
|
||||
}
|
||||
else if (options.LogAllApiRequests)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"API request completed: {Method} {Path} ({Endpoint}) returned {StatusCode} in {DurationMs:F1} ms, request {RequestId}.",
|
||||
context.Request.Method,
|
||||
context.Request.Path.Value,
|
||||
endpoint,
|
||||
context.Response.StatusCode,
|
||||
durationMilliseconds,
|
||||
context.TraceIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Data.Common;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
@@ -9,31 +7,10 @@ namespace Jiaowu.Api.Infrastructure.Observability;
|
||||
|
||||
public sealed class DatabaseCommandTelemetryInterceptor(
|
||||
ObservabilityOptions options,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<DatabaseCommandTelemetryInterceptor> logger)
|
||||
: DbCommandInterceptor
|
||||
{
|
||||
public const string ActivitySourceName = "Jiaowu.Api.Database";
|
||||
public const string MeterName = "Jiaowu.Api.Database";
|
||||
|
||||
private static readonly ActivitySource ActivitySource =
|
||||
new(ActivitySourceName);
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
private static readonly Histogram<double> CommandDuration =
|
||||
Meter.CreateHistogram<double>(
|
||||
"jiaowu.db.command.duration",
|
||||
"ms",
|
||||
"EF Core database command duration");
|
||||
private static readonly Counter<long> SlowCommandCount =
|
||||
Meter.CreateCounter<long>(
|
||||
"jiaowu.db.command.slow",
|
||||
"{command}",
|
||||
"EF Core commands exceeding the configured slow-query threshold");
|
||||
private static readonly Counter<long> FailedCommandCount =
|
||||
Meter.CreateCounter<long>(
|
||||
"jiaowu.db.command.failed",
|
||||
"{command}",
|
||||
"Failed EF Core database commands");
|
||||
|
||||
public override DbDataReader ReaderExecuted(
|
||||
DbCommand command,
|
||||
CommandExecutedEventData eventData,
|
||||
@@ -138,82 +115,41 @@ public sealed class DatabaseCommandTelemetryInterceptor(
|
||||
var queryName = GetQueryName(command.CommandText);
|
||||
var statementHash = GetStatementHash(command.CommandText);
|
||||
var provider = GetProviderName(command);
|
||||
var traceId = Activity.Current?.TraceId.ToString() ?? "none";
|
||||
var tags = new TagList
|
||||
{
|
||||
{ "db.system.name", provider },
|
||||
{ "db.operation.name", commandKind },
|
||||
{ "db.query.name", queryName }
|
||||
};
|
||||
if (errorType is not null)
|
||||
tags.Add("error.type", errorType);
|
||||
|
||||
var requestId = httpContextAccessor.HttpContext?.TraceIdentifier ?? "background";
|
||||
var durationMilliseconds = duration.TotalMilliseconds;
|
||||
CommandDuration.Record(durationMilliseconds, tags);
|
||||
if (errorType is not null)
|
||||
FailedCommandCount.Add(1, tags);
|
||||
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
ActivityKind.Client,
|
||||
Activity.Current?.Context ?? default,
|
||||
startTime: DateTimeOffset.UtcNow - duration,
|
||||
name: queryName);
|
||||
if (activity is not null)
|
||||
{
|
||||
activity.SetTag("db.system.name", provider);
|
||||
activity.SetTag("db.operation.name", commandKind);
|
||||
activity.SetTag("db.query.name", queryName);
|
||||
activity.SetTag("db.statement.hash", statementHash);
|
||||
activity.SetTag(
|
||||
"db.namespace",
|
||||
EmptyToNull(command.Connection?.Database));
|
||||
if (options.IncludeSqlText)
|
||||
{
|
||||
activity.SetTag(
|
||||
"db.query.text",
|
||||
Truncate(command.CommandText, options.MaximumSqlTextLength));
|
||||
}
|
||||
if (errorType is not null)
|
||||
{
|
||||
activity.SetTag("error.type", errorType);
|
||||
activity.SetStatus(ActivityStatusCode.Error, errorType);
|
||||
}
|
||||
activity.SetEndTime(DateTime.UtcNow);
|
||||
}
|
||||
|
||||
if (errorType is not null)
|
||||
{
|
||||
logger.LogError(
|
||||
"Database command failed after {DurationMs:F1} ms: " +
|
||||
"{QueryName} ({CommandKind}, {Provider}, hash {StatementHash}, " +
|
||||
"error {ErrorType}, trace {TraceId}).",
|
||||
"error {ErrorType}, request {RequestId}).",
|
||||
durationMilliseconds,
|
||||
queryName,
|
||||
commandKind,
|
||||
provider,
|
||||
statementHash,
|
||||
errorType,
|
||||
traceId);
|
||||
requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (durationMilliseconds < options.SlowQueryThresholdMilliseconds)
|
||||
return;
|
||||
|
||||
SlowCommandCount.Add(1, tags);
|
||||
if (options.IncludeSqlText)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"Slow database command took {DurationMs:F1} ms: " +
|
||||
"{QueryName} ({CommandKind}, {Provider}, hash {StatementHash}, " +
|
||||
"trace {TraceId}). " +
|
||||
"request {RequestId}). " +
|
||||
"SQL template: {SqlTemplate}",
|
||||
durationMilliseconds,
|
||||
queryName,
|
||||
commandKind,
|
||||
provider,
|
||||
statementHash,
|
||||
traceId,
|
||||
requestId,
|
||||
Truncate(command.CommandText, options.MaximumSqlTextLength));
|
||||
}
|
||||
else
|
||||
@@ -221,13 +157,13 @@ public sealed class DatabaseCommandTelemetryInterceptor(
|
||||
logger.LogWarning(
|
||||
"Slow database command took {DurationMs:F1} ms: " +
|
||||
"{QueryName} ({CommandKind}, {Provider}, hash {StatementHash}, " +
|
||||
"trace {TraceId}).",
|
||||
"request {RequestId}).",
|
||||
durationMilliseconds,
|
||||
queryName,
|
||||
commandKind,
|
||||
provider,
|
||||
statementHash,
|
||||
traceId);
|
||||
requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,9 +207,6 @@ public sealed class DatabaseCommandTelemetryInterceptor(
|
||||
return "other_sql";
|
||||
}
|
||||
|
||||
private static string? EmptyToNull(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
|
||||
private static string Truncate(string value, int maximumLength) =>
|
||||
value.Length <= maximumLength
|
||||
? value
|
||||
|
||||
@@ -6,6 +6,8 @@ public sealed class ObservabilityOptions
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string ServiceName { get; set; } = "jiaowu-api";
|
||||
public bool LogAllApiRequests { get; set; } = true;
|
||||
public int SlowRequestThresholdMilliseconds { get; set; } = 1000;
|
||||
public int SlowQueryThresholdMilliseconds { get; set; } = 500;
|
||||
public bool IncludeSqlText { get; set; }
|
||||
public int MaximumSqlTextLength { get; set; } = 2000;
|
||||
|
||||
@@ -47,11 +47,6 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
|
||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="10.0.9" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.8.0" />
|
||||
<PackageReference Include="RabbitMQ.Client" Version="7.2.2" />
|
||||
<PackageReference Include="SkiaSharp" Version="4.151.1" />
|
||||
|
||||
@@ -28,9 +28,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi;
|
||||
using OpenTelemetry.Metrics;
|
||||
using OpenTelemetry.Resources;
|
||||
using OpenTelemetry.Trace;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
@@ -179,6 +176,7 @@ if (databaseOptions.CommandTimeoutSeconds is < 5 or > 300)
|
||||
|
||||
if (string.IsNullOrWhiteSpace(observabilityOptions.ServiceName) ||
|
||||
observabilityOptions.ServiceName.Length > 100 ||
|
||||
observabilityOptions.SlowRequestThresholdMilliseconds is < 1 or > 60000 ||
|
||||
observabilityOptions.SlowQueryThresholdMilliseconds is < 1 or > 60000 ||
|
||||
observabilityOptions.MaximumSqlTextLength is < 256 or > 20000)
|
||||
{
|
||||
@@ -208,15 +206,6 @@ if (performanceReportingOptions.CacheSeconds is < 5 or > 300 ||
|
||||
"PerformanceReporting 数据源、超时、缓存或指标名称配置无效。");
|
||||
}
|
||||
|
||||
var otlpEndpoint = builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"];
|
||||
if (!string.IsNullOrWhiteSpace(otlpEndpoint) &&
|
||||
(!Uri.TryCreate(otlpEndpoint, UriKind.Absolute, out var parsedOtlpEndpoint) ||
|
||||
parsedOtlpEndpoint.Scheme is not ("http" or "https")))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"OTEL_EXPORTER_OTLP_ENDPOINT 必须是有效的 HTTP 或 HTTPS 绝对地址。");
|
||||
}
|
||||
|
||||
if (cacheOptions.ReferenceExpirationMinutes is < 1 or > 1440 ||
|
||||
cacheOptions.TimetableExpirationMinutes is < 1 or > 1440 ||
|
||||
cacheOptions.AnalyticsExpirationMinutes is < 1 or > 1440 ||
|
||||
@@ -319,6 +308,7 @@ builder.Services.AddSingleton(performanceReportingOptions);
|
||||
builder.Services.AddSingleton(clickHouseAnalyticsOptions);
|
||||
builder.Services.AddSingleton(rabbitMqOptions);
|
||||
builder.Services.AddSingleton<DatabaseCommandTelemetryInterceptor>();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddHttpClient<PerformanceReportService>((services, client) =>
|
||||
{
|
||||
@@ -379,28 +369,6 @@ builder.Services.AddDbContextPool<AppDbContext>((services, options) =>
|
||||
});
|
||||
});
|
||||
|
||||
if (observabilityOptions.Enabled &&
|
||||
!string.IsNullOrWhiteSpace(otlpEndpoint))
|
||||
{
|
||||
builder.Services
|
||||
.AddOpenTelemetry()
|
||||
.ConfigureResource(resource =>
|
||||
resource.AddService(observabilityOptions.ServiceName))
|
||||
.WithMetrics(metrics => metrics
|
||||
.AddAspNetCoreInstrumentation()
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddRuntimeInstrumentation()
|
||||
.AddMeter(DatabaseCommandTelemetryInterceptor.MeterName))
|
||||
.WithTracing(tracing => tracing
|
||||
.AddAspNetCoreInstrumentation(options =>
|
||||
options.Filter = context =>
|
||||
!context.Request.Path.StartsWithSegments("/health/live"))
|
||||
.AddHttpClientInstrumentation()
|
||||
.AddSource(DatabaseCommandTelemetryInterceptor.ActivitySourceName))
|
||||
.WithMetrics(metrics => metrics.AddOtlpExporter())
|
||||
.WithTracing(tracing => tracing.AddOtlpExporter());
|
||||
}
|
||||
|
||||
var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
|
||||
if (cacheOptions.Enabled && !string.IsNullOrWhiteSpace(redisConnectionString))
|
||||
{
|
||||
@@ -753,6 +721,7 @@ app.UseStaticFiles(new StaticFileOptions
|
||||
});
|
||||
app.UseCors("Web");
|
||||
app.UseRateLimiter();
|
||||
app.UseMiddleware<SlowRequestLoggingMiddleware>();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseMiddleware<AuditMiddleware>();
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
"Observability": {
|
||||
"Enabled": true,
|
||||
"ServiceName": "jiaowu-api",
|
||||
"LogAllApiRequests": true,
|
||||
"SlowRequestThresholdMilliseconds": 1000,
|
||||
"SlowQueryThresholdMilliseconds": 500,
|
||||
"IncludeSqlText": false,
|
||||
"MaximumSqlTextLength": 2000
|
||||
|
||||
Reference in New Issue
Block a user