接入 OpenTelemetry 1.17.0,覆盖 HTTP、HttpClient、.NET Runtime 和数据库链路。

新增 EF Core 数据库拦截器,记录耗时、失败数、慢查询数、TraceId、查询标签和 SQL 哈希;默认不记录完整 SQL及参数。
未配置 OTLP Collector 时不启动 SDK,避免无收益的性能开销。
为课表的作息、课程、灵活课程、考试、实验等六类查询增加稳定标签。
增加可配置的 500ms 慢查询阈值,以及生产环境变量示例。
README 补充 MySQL 慢查询与 EXPLAIN ANALYZE 操作规范。
This commit is contained in:
2026-07-31 08:24:27 +08:00 Unverified
parent a369b4fe02
commit ca7148ab61
9 changed files with 489 additions and 1 deletions
+53 -1
View File
@@ -7,6 +7,7 @@ using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Exams;
using Jiaowu.Api.Infrastructure.Middleware;
using Jiaowu.Api.Infrastructure.Observability;
using Jiaowu.Api.Infrastructure.OfficialDocuments;
using Jiaowu.Api.Infrastructure.Operations;
using Jiaowu.Api.Infrastructure.Persistence;
@@ -19,6 +20,9 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System.Threading.RateLimiting;
EnvironmentFile.Load();
@@ -78,6 +82,9 @@ var backgroundJobOptions = builder.Configuration
var operationsOptions = builder.Configuration
.GetSection(OperationsOptions.SectionName)
.Get<OperationsOptions>() ?? new OperationsOptions();
var observabilityOptions = builder.Configuration
.GetSection(ObservabilityOptions.SectionName)
.Get<ObservabilityOptions>() ?? new ObservabilityOptions();
var rabbitMqOptions = builder.Configuration
.GetSection(RabbitMqOptions.SectionName)
.Get<RabbitMqOptions>() ?? new RabbitMqOptions();
@@ -110,6 +117,24 @@ if (databaseOptions.CommandTimeoutSeconds is < 5 or > 300)
"Database:CommandTimeoutSeconds 必须在 5 到 300 秒之间。");
}
if (string.IsNullOrWhiteSpace(observabilityOptions.ServiceName) ||
observabilityOptions.ServiceName.Length > 100 ||
observabilityOptions.SlowQueryThresholdMilliseconds is < 1 or > 60000 ||
observabilityOptions.MaximumSqlTextLength is < 256 or > 20000)
{
throw new InvalidOperationException(
"Observability 服务名、慢查询阈值或 SQL 文本长度超出允许范围。");
}
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 ||
@@ -193,11 +218,16 @@ builder.Services.AddSingleton(cacheOptions);
builder.Services.AddSingleton(officialDocumentOptions);
builder.Services.AddSingleton(backgroundJobOptions);
builder.Services.AddSingleton(operationsOptions);
builder.Services.AddSingleton(observabilityOptions);
builder.Services.AddSingleton(rabbitMqOptions);
builder.Services.AddSingleton<DatabaseCommandTelemetryInterceptor>();
builder.Services.Configure<OfficialDocumentOptions>(
builder.Configuration.GetSection(OfficialDocumentOptions.SectionName));
builder.Services.AddDbContextPool<AppDbContext>(options =>
builder.Services.AddDbContextPool<AppDbContext>((services, options) =>
{
options.AddInterceptors(
services.GetRequiredService<DatabaseCommandTelemetryInterceptor>());
if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase))
{
var sqliteConnectionString = builder.Configuration.GetConnectionString("SQLite")
@@ -240,6 +270,28 @@ builder.Services.AddDbContextPool<AppDbContext>(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))
{