接入 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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user