接入 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
@@ -0,0 +1,88 @@
using System.Diagnostics.Metrics;
using Jiaowu.Api.Infrastructure.Observability;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
namespace Jiaowu.Api.Tests;
public sealed class DatabaseCommandTelemetryInterceptorTests
{
[Fact]
public void Query_name_uses_tag_without_exposing_statement_text()
{
const string sql =
"-- Timetable.LoadMixedExamEntries\n" +
"SELECT * FROM ExamRooms WHERE SecretValue = @p0";
var queryName =
DatabaseCommandTelemetryInterceptor.GetQueryName(sql);
Assert.Equal("Timetable.LoadMixedExamEntries", queryName);
Assert.DoesNotContain("SecretValue", queryName);
Assert.DoesNotContain("@p0", queryName);
}
[Fact]
public void Untagged_query_name_is_stable_hash_not_statement_text()
{
const string sql =
"SELECT * FROM Students WHERE StudentNumber = @studentNumber";
var first = DatabaseCommandTelemetryInterceptor.GetQueryName(sql);
var second = DatabaseCommandTelemetryInterceptor.GetQueryName(sql);
Assert.Equal(first, second);
Assert.StartsWith("select:", first);
Assert.DoesNotContain("Students", first);
Assert.DoesNotContain("StudentNumber", first);
}
[Fact]
public async Task Ef_command_records_duration_metric()
{
await using var connection =
new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var setupOptions = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
await using (var setupDb = new AppDbContext(setupOptions))
await setupDb.Database.EnsureCreatedAsync();
double? recordedDuration = null;
using var listener = new MeterListener
{
InstrumentPublished = (instrument, meterListener) =>
{
if (instrument.Meter.Name ==
DatabaseCommandTelemetryInterceptor.MeterName &&
instrument.Name == "jiaowu.db.command.duration")
{
meterListener.EnableMeasurementEvents(instrument);
}
}
};
listener.SetMeasurementEventCallback<double>(
(_, measurement, _, _) => recordedDuration = measurement);
listener.Start();
var interceptor = new DatabaseCommandTelemetryInterceptor(
new ObservabilityOptions(),
NullLogger<DatabaseCommandTelemetryInterceptor>.Instance);
var queryOptions = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.AddInterceptors(interceptor)
.Options;
await using var db = new AppDbContext(queryOptions);
await db.AcademicTerms
.TagWith("Observability.Tests.TermCount")
.CountAsync();
Assert.NotNull(recordedDuration);
Assert.True(recordedDuration >= 0);
}
}