Skip to content

Commit 30b7917

Browse files
committed
feat(RequestTrace): refactor RequestTrace schema and middleware for enhanced tracking timestamps
1 parent 815d8c0 commit 30b7917

9 files changed

Lines changed: 45 additions & 51 deletions

File tree

src/BE/db/ChatsDB.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -360,11 +360,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
360360
.HasConstraintName("FK_Prompt_CreateUserId");
361361
});
362362

363-
modelBuilder.Entity<RequestTrace>(entity =>
364-
{
365-
entity.HasOne(d => d.User).WithMany(p => p.RequestTraces).HasConstraintName("FK_RequestTrace_User");
366-
});
367-
368363
modelBuilder.Entity<RequestTracePayload>(entity =>
369364
{
370365
entity.Property(e => e.LogId).ValueGeneratedNever();

src/BE/db/RequestTrace.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace Chats.DB;
88

99
[Table("RequestTrace")]
1010
[Index("StartedAt", Name = "IX_RequestTrace_StartedAt")]
11+
[Index("TraceId", Name = "IX_RequestTrace_TraceId")]
1112
[Index("UserId", Name = "IX_RequestTrace_UserId")]
1213
public partial class RequestTrace
1314
{
@@ -16,7 +17,11 @@ public partial class RequestTrace
1617

1718
public DateTime StartedAt { get; set; }
1819

19-
public int DurationMs { get; set; }
20+
public DateTime? RequestBodyAt { get; set; }
21+
22+
public DateTime? ResponseHeaderAt { get; set; }
23+
24+
public DateTime? ResponseBodyAt { get; set; }
2025

2126
public byte Direction { get; set; }
2227

@@ -26,6 +31,7 @@ public partial class RequestTrace
2631
public int? UserId { get; set; }
2732

2833
[StringLength(100)]
34+
[Unicode(false)]
2935
public string? TraceId { get; set; }
3036

3137
[StringLength(10)]
@@ -36,9 +42,11 @@ public partial class RequestTrace
3642
public string Url { get; set; } = null!;
3743

3844
[StringLength(200)]
45+
[Unicode(false)]
3946
public string? RequestContentType { get; set; }
4047

4148
[StringLength(200)]
49+
[Unicode(false)]
4250
public string? ResponseContentType { get; set; }
4351

4452
public short? StatusCode { get; set; }
@@ -58,8 +66,4 @@ public partial class RequestTrace
5866

5967
[InverseProperty("Log")]
6068
public virtual RequestTracePayload? RequestTracePayload { get; set; }
61-
62-
[ForeignKey("UserId")]
63-
[InverseProperty("RequestTraces")]
64-
public virtual User? User { get; set; }
6569
}

src/BE/db/RequestTracePayload.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ public partial class RequestTracePayload
1212
[Key]
1313
public long LogId { get; set; }
1414

15+
[Unicode(false)]
1516
public string RequestHeaders { get; set; } = null!;
1617

18+
[Unicode(false)]
1719
public string? ResponseHeaders { get; set; }
1820

1921
public string? RequestBody { get; set; }

src/BE/db/User.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,6 @@ public partial class User
7777
[InverseProperty("CreateUser")]
7878
public virtual ICollection<Prompt> Prompts { get; set; } = new List<Prompt>();
7979

80-
[InverseProperty("User")]
81-
public virtual ICollection<RequestTrace> RequestTraces { get; set; } = new List<RequestTrace>();
82-
8380
[InverseProperty("User")]
8481
public virtual ICollection<SmsRecord> SmsRecords { get; set; } = new List<SmsRecord>();
8582

src/BE/web/Infrastructure/RequestTraceMiddleware.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Chats.BE.Services.RequestTracing;
33
using Chats.BE.Services.Sessions;
44
using Chats.BE.Services.UrlEncryption;
5-
using System.Diagnostics;
65
using System.Security.Claims;
76

87
namespace Chats.BE.Infrastructure;
@@ -24,7 +23,6 @@ public async Task Invoke(HttpContext context)
2423
}
2524

2625
DateTime startedAt = DateTime.UtcNow;
27-
long startTick = Stopwatch.GetTimestamp();
2826
string method = context.Request.Method;
2927
string url = context.Request.Path + context.Request.QueryString;
3028
string? source = context.Connection.RemoteIpAddress?.ToString();
@@ -83,6 +81,7 @@ public async Task Invoke(HttpContext context)
8381
RequestTraceRequestBodyWriteModel requestBodyModel = new()
8482
{
8583
StartedAt = startedAt,
84+
RequestBodyAt = DateTime.UtcNow,
8685
Direction = RequestTraceDirection.Inbound,
8786
Source = source,
8887
UserId = userId,
@@ -120,7 +119,6 @@ public async Task Invoke(HttpContext context)
120119
{
121120
try
122121
{
123-
int durationMs = (int)Stopwatch.GetElapsedTime(startTick, Stopwatch.GetTimestamp()).TotalMilliseconds;
124122
short? statusCode = (short?)context.Response.StatusCode;
125123
string? responseHeaders = RequestTraceHelper.FormatHeaders(
126124
context.Response.Headers.Select(x => new KeyValuePair<string, IEnumerable<string>>(x.Key, x.Value.Select(v => v ?? string.Empty))),
@@ -130,13 +128,13 @@ public async Task Invoke(HttpContext context)
130128
RequestTraceResponseHeaderWriteModel responseHeaderModel = new()
131129
{
132130
StartedAt = startedAt,
131+
ResponseHeaderAt = DateTime.UtcNow,
133132
Direction = RequestTraceDirection.Inbound,
134133
Source = source,
135134
UserId = userId,
136135
TraceId = traceId,
137136
Method = method,
138137
Url = url,
139-
DurationMs = durationMs,
140138
ResponseContentType = context.Response.ContentType,
141139
StatusCode = statusCode,
142140
ErrorType = null,
@@ -164,7 +162,6 @@ public async Task Invoke(HttpContext context)
164162
{
165163
byte[]? responseBytes = tee?.CapturedBytes;
166164
bool responseBodyTruncated = tee?.IsTruncated == true;
167-
int durationMs = (int)Stopwatch.GetElapsedTime(startTick, Stopwatch.GetTimestamp()).TotalMilliseconds;
168165
short? statusCode = (short?)context.Response.StatusCode;
169166

170167
(string? responseText, bool responseTextTruncated) = config.Body.CaptureResponseBody
@@ -179,13 +176,13 @@ public async Task Invoke(HttpContext context)
179176
RequestTraceResponseBodyWriteModel responseBodyModel = new()
180177
{
181178
StartedAt = startedAt,
179+
ResponseBodyAt = DateTime.UtcNow,
182180
Direction = RequestTraceDirection.Inbound,
183181
Source = source,
184182
UserId = userId,
185183
TraceId = traceId,
186184
Method = method,
187185
Url = url,
188-
DurationMs = durationMs,
189186
ResponseContentType = context.Response.ContentType,
190187
StatusCode = statusCode,
191188
ErrorType = exception?.GetType().Name,

src/BE/web/Services/RequestTracing/OutboundRequestTraceHandler.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
7676
RequestTraceRequestBodyWriteModel requestBodyModel = new()
7777
{
7878
StartedAt = startedAt,
79+
RequestBodyAt = DateTime.UtcNow,
7980
Direction = RequestTraceDirection.Outbound,
8081
Source = source,
8182
UserId = userId,
@@ -126,13 +127,13 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
126127
RequestTraceResponseHeaderWriteModel responseHeaderModel = new()
127128
{
128129
StartedAt = startedAt,
130+
ResponseHeaderAt = DateTime.UtcNow,
129131
Direction = RequestTraceDirection.Outbound,
130132
Source = source,
131133
UserId = userId,
132134
TraceId = null,
133135
Method = method,
134136
Url = url,
135-
DurationMs = durationMs,
136137
ResponseContentType = response?.Content?.Headers.ContentType?.ToString(),
137138
StatusCode = statusCode,
138139
ErrorType = exception?.GetType().Name,
@@ -164,7 +165,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
164165
RequestTraceResponseBodyWriteModel responseBodyModel = new()
165166
{
166167
StartedAt = startedAt,
167-
DurationMs = durationMs,
168+
ResponseBodyAt = DateTime.UtcNow,
168169
Direction = RequestTraceDirection.Outbound,
169170
Source = source,
170171
UserId = userId,

src/BE/web/Services/RequestTracing/RequestTracePersistService.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ private async Task PersistRequestHeaderAsync(RequestTraceRequestHeaderWriteModel
4545
RequestTrace trace = new()
4646
{
4747
StartedAt = item.StartedAt,
48-
DurationMs = 0,
48+
RequestBodyAt = null,
49+
ResponseHeaderAt = null,
50+
ResponseBodyAt = null,
4951
Direction = (byte)item.Direction,
5052
Source = item.Source,
5153
UserId = item.UserId,
@@ -94,6 +96,7 @@ private async Task PersistRequestBodyAsync(RequestTraceRequestBodyWriteModel ite
9496
}
9597

9698
trace.RequestContentType = item.RequestContentType;
99+
trace.RequestBodyAt = item.RequestBodyAt;
97100
trace.RawRequestBodyBytes = item.RawRequestBodyBytes;
98101
trace.IsRequestBodyTruncated = item.IsRequestBodyTruncated;
99102

@@ -118,7 +121,7 @@ private async Task PersistResponseHeaderAsync(RequestTraceResponseHeaderWriteMod
118121
ChatsDB db = scope.ServiceProvider.GetRequiredService<ChatsDB>();
119122

120123
List<RequestTrace> matches = await QueryCandidate(db, item)
121-
.Where(x => x.StatusCode == null && x.DurationMs == 0)
124+
.Where(x => x.StatusCode == null && x.ResponseHeaderAt == null)
122125
.OrderByDescending(x => x.Id)
123126
.Take(2)
124127
.ToListAsync(cancellationToken);
@@ -137,7 +140,7 @@ private async Task PersistResponseHeaderAsync(RequestTraceResponseHeaderWriteMod
137140
}
138141

139142
RequestTrace trace = matches[0];
140-
trace.DurationMs = item.DurationMs;
143+
trace.ResponseHeaderAt = item.ResponseHeaderAt;
141144
trace.ResponseContentType = item.ResponseContentType;
142145
trace.StatusCode = item.StatusCode;
143146
trace.ErrorType = item.ErrorType;
@@ -182,7 +185,7 @@ private async Task PersistResponseBodyAsync(RequestTraceResponseBodyWriteModel i
182185
}
183186

184187
RequestTrace trace = matches[0];
185-
trace.DurationMs = item.DurationMs;
188+
trace.ResponseBodyAt = item.ResponseBodyAt;
186189
trace.ResponseContentType = item.ResponseContentType;
187190
trace.StatusCode = item.StatusCode;
188191
trace.ErrorType = item.ErrorType;

src/BE/web/Services/RequestTracing/RequestTraceWriteModel.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public sealed class RequestTraceRequestHeaderWriteModel : RequestTraceWriteModel
1919

2020
public sealed class RequestTraceRequestBodyWriteModel : RequestTraceWriteModel
2121
{
22+
public DateTime RequestBodyAt { get; init; }
2223
public string? RequestContentType { get; init; }
2324
public int RawRequestBodyBytes { get; init; }
2425
public bool IsRequestBodyTruncated { get; init; }
@@ -28,7 +29,7 @@ public sealed class RequestTraceRequestBodyWriteModel : RequestTraceWriteModel
2829

2930
public sealed class RequestTraceResponseHeaderWriteModel : RequestTraceWriteModel
3031
{
31-
public int DurationMs { get; init; }
32+
public DateTime ResponseHeaderAt { get; init; }
3233
public string? ResponseContentType { get; init; }
3334
public short? StatusCode { get; init; }
3435
public string? ErrorType { get; init; }
@@ -38,7 +39,7 @@ public sealed class RequestTraceResponseHeaderWriteModel : RequestTraceWriteMode
3839

3940
public sealed class RequestTraceResponseBodyWriteModel : RequestTraceWriteModel
4041
{
41-
public int DurationMs { get; init; }
42+
public DateTime ResponseBodyAt { get; init; }
4243
public string? ResponseContentType { get; init; }
4344
public short? StatusCode { get; init; }
4445
public string? ErrorType { get; init; }

src/scripts/db-migration/1.11/1.11.0.sql

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ BEGIN
1111
(
1212
Id BIGINT NOT NULL IDENTITY(1, 1), -- 日志主键(高写入场景使用 BIGINT 自增)
1313
StartedAt DATETIME2(7) NOT NULL, -- 请求开始时间(UTC)
14-
DurationMs INT NOT NULL, -- 耗时(毫秒)
14+
RequestBodyAt DATETIME2(7) NULL, -- 请求体读取完成时间(UTC)
15+
ResponseHeaderAt DATETIME2(7) NULL, -- 响应头接收完成时间(UTC)
16+
ResponseBodyAt DATETIME2(7) NULL, -- 响应体读取完成时间(UTC)
1517
Direction TINYINT NOT NULL, -- 方向:0=Inbound(用户->后端),1=Outbound(后端->外部)
1618
Source NVARCHAR(100) NULL, -- 来源标识;入站可放来源IP,出站可放命名HttpClient或目标来源标签(100 足够覆盖 IPv6 文本)
17-
UserId INT NULL, -- 关联用户;但保留索引+外键
18-
TraceId NVARCHAR(100) NULL, -- 串联键:用于把同一链路日志聚合;按我们约定可直接使用 HttpContext.TraceIdentifier
19+
UserId INT NULL, -- 关联用户;保留索引,不创建外键
20+
TraceId VARCHAR(100) NULL, -- 串联键:用于把同一链路日志聚合;按我们约定可直接使用 HttpContext.TraceIdentifier
1921
[Method] VARCHAR(10) NOT NULL, -- HTTP 方法(GET/POST/...)
2022
[Url] NVARCHAR(2048) NOT NULL, -- 完整 URL;已去掉 Host/Path/Query 拆列,避免冗余字段
21-
RequestContentType NVARCHAR(200) NULL, -- 请求体 Content-Type
22-
ResponseContentType NVARCHAR(200) NULL, -- 响应体 Content-Type
23+
RequestContentType VARCHAR(200) NULL, -- 请求体 Content-Type
24+
ResponseContentType VARCHAR(200) NULL, -- 响应体 Content-Type
2325
StatusCode SMALLINT NULL, -- 响应状态码;无响应异常场景允许为 NULL(不再使用 HasResponse)
2426
ErrorType NVARCHAR(50) NULL, -- 异常类型(建议枚举字符串,如 Timeout/Dns/Connect/UnhandledException)
2527
ErrorMessage NVARCHAR(MAX) NULL, -- 异常详情;inbound 可存 Exception.ToString(),outbound 通常为空
@@ -78,27 +80,19 @@ BEGIN
7880
PRINT N' -> 索引 IX_RequestTrace_UserId 已存在,跳过';
7981
END
8082

81-
IF OBJECT_ID(N'dbo.[User]', N'U') IS NOT NULL
83+
IF NOT EXISTS (
84+
SELECT 1 FROM sys.indexes
85+
WHERE name = N'IX_RequestTrace_TraceId'
86+
AND object_id = OBJECT_ID(N'dbo.RequestTrace')
87+
)
8288
BEGIN
83-
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = N'FK_RequestTrace_User')
84-
BEGIN
85-
ALTER TABLE dbo.RequestTrace
86-
WITH CHECK ADD CONSTRAINT FK_RequestTrace_User
87-
FOREIGN KEY (UserId) REFERENCES dbo.[User](Id);
88-
89-
ALTER TABLE dbo.RequestTrace
90-
CHECK CONSTRAINT FK_RequestTrace_User;
91-
92-
PRINT N' -> 已创建外键 FK_RequestTrace_User';
93-
END
94-
ELSE
95-
BEGIN
96-
PRINT N' -> 外键 FK_RequestTrace_User 已存在,跳过';
97-
END
89+
CREATE INDEX IX_RequestTrace_TraceId
90+
ON dbo.RequestTrace (TraceId);
91+
PRINT N' -> 已创建索引 IX_RequestTrace_TraceId';
9892
END
9993
ELSE
10094
BEGIN
101-
PRINT N' -> dbo.[User] 表不存在,跳过 FK_RequestTrace_User 创建';
95+
PRINT N' -> 索引 IX_RequestTrace_TraceId 已存在,跳过';
10296
END
10397

10498
END
@@ -119,8 +113,8 @@ BEGIN
119113
CREATE TABLE dbo.RequestTracePayload
120114
(
121115
LogId BIGINT NOT NULL, -- 对应主表 RequestTrace.Id(1:1)
122-
RequestHeaders NVARCHAR(MAX) NOT NULL, -- 请求头原始文本(按原样存储,建议使用 RFC 风格多行 header 文本;由应用层保证写入)
123-
ResponseHeaders NVARCHAR(MAX) NULL, -- 响应头原始文本(按原样存储;无响应时可为空)
116+
RequestHeaders VARCHAR(MAX) NOT NULL, -- 请求头原始文本(按原样存储,建议使用 RFC 风格多行 header 文本;由应用层保证写入)
117+
ResponseHeaders VARCHAR(MAX) NULL, -- 响应头原始文本(按原样存储;无响应时可为空)
124118
RequestBody NVARCHAR(MAX) NULL, -- 请求体文本(可检索、业务友好)
125119
ResponseBody NVARCHAR(MAX) NULL, -- 响应体文本(可检索、业务友好)
126120
RequestBodyRaw VARBINARY(MAX) NULL, -- 原始请求体二进制(未解压、未de-chunk、尽量原样;按配置开启)

0 commit comments

Comments
 (0)