Skip to content

Commit 4c66a82

Browse files
committed
refactor(request-tracing): switch to Guid-linked updates, improve queue observability, and short-circuit tracing on entry drop
generate and propagate Guid.CreateVersion7() LogId across inbound/outbound trace stages persist updates by LogId via ExecuteUpdateAsync (with InMemory fallback retained) align request-trace admin/API IDs to Guid rename/move inbound tracer to InboundRequestTraceMiddleware under request-tracing services add queue metrics (DroppedCount, QueuedCount, QueueHighWatermark) and enrich drop logs with logId/traceId raise queue-drop logs to warning and skip downstream tracing when entry request-header enqueue fails only update payload body/raw columns when non-null in non-InMemory paths
1 parent 076644f commit 4c66a82

13 files changed

Lines changed: 450 additions & 212 deletions

File tree

src/BE/db/ChatsDB.cs

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

363+
modelBuilder.Entity<RequestTrace>(entity =>
364+
{
365+
entity.Property(e => e.Id).ValueGeneratedNever();
366+
});
367+
363368
modelBuilder.Entity<RequestTracePayload>(entity =>
364369
{
365370
entity.Property(e => e.LogId).ValueGeneratedNever();

src/BE/db/RequestTrace.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Chats.DB;
1313
public partial class RequestTrace
1414
{
1515
[Key]
16-
public long Id { get; set; }
16+
public Guid Id { get; set; }
1717

1818
public DateTime StartedAt { get; set; }
1919

src/BE/db/RequestTracePayload.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Chats.DB;
1010
public partial class RequestTracePayload
1111
{
1212
[Key]
13-
public long LogId { get; set; }
13+
public Guid LogId { get; set; }
1414

1515
[Unicode(false)]
1616
public string RequestHeaders { get; set; } = null!;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using Chats.BE.Services.RequestTracing;
2+
using Chats.DB;
3+
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Logging.Abstractions;
6+
7+
namespace Chats.BE.UnitTest.Services.RequestTracing;
8+
9+
public sealed class RequestTracePersistServiceTests
10+
{
11+
private static ServiceProvider CreateServiceProvider(string dbName)
12+
{
13+
ServiceCollection services = new();
14+
services.AddDbContext<ChatsDB>(o => o.UseInMemoryDatabase(dbName));
15+
return services.BuildServiceProvider();
16+
}
17+
18+
[Fact]
19+
public async Task PersistSingleAsync_InMemoryBranch_UpdatesByLogIdWithoutCandidateQuery()
20+
{
21+
ServiceProvider sp = CreateServiceProvider(nameof(PersistSingleAsync_InMemoryBranch_UpdatesByLogIdWithoutCandidateQuery));
22+
RequestTracePersistService service = new(
23+
new RequestTraceQueue(),
24+
sp.GetRequiredService<IServiceScopeFactory>(),
25+
NullLogger<RequestTracePersistService>.Instance);
26+
27+
Guid logId = Guid.CreateVersion7();
28+
DateTime startedAt = DateTime.UtcNow;
29+
30+
await service.PersistSingleAsync(new RequestTraceRequestHeaderWriteModel
31+
{
32+
LogId = logId,
33+
StartedAt = startedAt,
34+
Direction = RequestTraceDirection.Inbound,
35+
Source = "127.0.0.1",
36+
UserId = 1,
37+
TraceId = "trace-1",
38+
Method = "GET",
39+
Url = "/v1/test",
40+
RequestContentType = "application/json",
41+
RequestHeaders = "x-a: 1"
42+
}, CancellationToken.None);
43+
44+
await service.PersistSingleAsync(new RequestTraceRequestBodyWriteModel
45+
{
46+
LogId = logId,
47+
StartedAt = startedAt.AddMinutes(1),
48+
Direction = RequestTraceDirection.Outbound,
49+
Source = "different-source",
50+
UserId = 2,
51+
TraceId = "trace-2",
52+
Method = "POST",
53+
Url = "/mismatch",
54+
RequestBodyAt = startedAt.AddSeconds(1),
55+
RequestContentType = "text/plain",
56+
RawRequestBodyBytes = 11,
57+
IsRequestBodyTruncated = true,
58+
RequestBody = "hello world",
59+
RequestBodyRaw = [1, 2, 3]
60+
}, CancellationToken.None);
61+
62+
await service.PersistSingleAsync(new RequestTraceResponseHeaderWriteModel
63+
{
64+
LogId = logId,
65+
StartedAt = startedAt.AddMinutes(2),
66+
Direction = RequestTraceDirection.Outbound,
67+
Source = "different-source-2",
68+
UserId = 3,
69+
TraceId = "trace-3",
70+
Method = "PATCH",
71+
Url = "/mismatch-2",
72+
ResponseHeaderAt = startedAt.AddSeconds(2),
73+
ResponseContentType = "application/json",
74+
StatusCode = 200,
75+
ResponseHeaders = "x-b: 2"
76+
}, CancellationToken.None);
77+
78+
await service.PersistSingleAsync(new RequestTraceResponseBodyWriteModel
79+
{
80+
LogId = logId,
81+
StartedAt = startedAt.AddMinutes(3),
82+
Direction = RequestTraceDirection.Outbound,
83+
Source = "different-source-3",
84+
UserId = 4,
85+
TraceId = "trace-4",
86+
Method = "DELETE",
87+
Url = "/mismatch-3",
88+
ResponseBodyAt = startedAt.AddSeconds(3),
89+
ResponseContentType = "application/json",
90+
StatusCode = 201,
91+
RawResponseBodyBytes = 7,
92+
IsResponseBodyTruncated = true,
93+
ResponseBody = "resp",
94+
ResponseBodyRaw = [4, 5, 6]
95+
}, CancellationToken.None);
96+
97+
await service.PersistSingleAsync(new RequestTraceExceptionWriteModel
98+
{
99+
LogId = logId,
100+
StartedAt = startedAt.AddMinutes(4),
101+
Direction = RequestTraceDirection.Outbound,
102+
Source = "different-source-4",
103+
UserId = 5,
104+
TraceId = "trace-5",
105+
Method = "PUT",
106+
Url = "/mismatch-4",
107+
ExceptionAt = startedAt.AddSeconds(4),
108+
ResponseContentType = "application/problem+json",
109+
StatusCode = 500,
110+
ErrorType = "TestException",
111+
ErrorMessage = "boom"
112+
}, CancellationToken.None);
113+
114+
using IServiceScope scope = sp.CreateScope();
115+
ChatsDB db = scope.ServiceProvider.GetRequiredService<ChatsDB>();
116+
RequestTrace trace = await db.RequestTraces.Include(x => x.RequestTracePayload).SingleAsync(x => x.Id == logId);
117+
118+
Assert.Equal("GET", trace.Method);
119+
Assert.Equal("/v1/test", trace.Url);
120+
Assert.Equal("text/plain", trace.RequestContentType);
121+
Assert.Equal(11, trace.RawRequestBodyBytes);
122+
Assert.True(trace.IsRequestBodyTruncated);
123+
124+
Assert.Equal("application/json", trace.ResponseContentType);
125+
Assert.Equal((short)201, trace.StatusCode);
126+
Assert.Equal(7, trace.RawResponseBodyBytes);
127+
Assert.True(trace.IsResponseBodyTruncated);
128+
129+
Assert.Equal("TestException", trace.ErrorType);
130+
Assert.Equal("boom", trace.ErrorMessage);
131+
132+
Assert.NotNull(trace.RequestTracePayload);
133+
Assert.Equal("x-a: 1", trace.RequestTracePayload!.RequestHeaders);
134+
Assert.Equal("x-b: 2", trace.RequestTracePayload.ResponseHeaders);
135+
Assert.Equal("hello world", trace.RequestTracePayload.RequestBody);
136+
Assert.Equal("resp", trace.RequestTracePayload.ResponseBody);
137+
Assert.Equal(new byte[] { 1, 2, 3 }, trace.RequestTracePayload.RequestBodyRaw);
138+
Assert.Equal(new byte[] { 4, 5, 6 }, trace.RequestTracePayload.ResponseBodyRaw);
139+
}
140+
}

src/BE/web/Controllers/Admin/RequestTrace/Dtos/RequestTraceListItemDto.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ namespace Chats.BE.Controllers.Admin.RequestTrace.Dtos;
22

33
public record RequestTraceListItemDto
44
{
5-
public required long Id { get; init; }
5+
public required Guid Id { get; init; }
66

77
public required DateTime StartedAt { get; init; }
88

src/BE/web/Controllers/Admin/RequestTrace/RequestTraceController.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public async Task<ActionResult<int>> DeleteByQuery([FromBody] RequestTraceExport
6161
return BadRequest(ModelState);
6262
}
6363

64-
long[] ids = await FilterRequestTraceEntity(query)
64+
Guid[] ids = await FilterRequestTraceEntity(query)
6565
.Select(x => x.Id)
6666
.ToArrayAsync(cancellationToken);
6767

@@ -81,8 +81,8 @@ await db.RequestTracePayloads
8181
return Ok(deleted);
8282
}
8383

84-
[HttpGet("{id:long}")]
85-
public async Task<ActionResult<RequestTraceDetailsDto>> GetDetails([FromRoute] long id, CancellationToken cancellationToken)
84+
[HttpGet("{id:guid}")]
85+
public async Task<ActionResult<RequestTraceDetailsDto>> GetDetails([FromRoute] Guid id, CancellationToken cancellationToken)
8686
{
8787
RequestTraceDetailsDto? details = await BuildDetailsQuery()
8888
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
@@ -95,8 +95,8 @@ public async Task<ActionResult<RequestTraceDetailsDto>> GetDetails([FromRoute] l
9595
return Ok(details);
9696
}
9797

98-
[HttpGet("{id:long}/dump")]
99-
public async Task<IActionResult> DownloadDump([FromRoute] long id, CancellationToken cancellationToken)
98+
[HttpGet("{id:guid}/dump")]
99+
public async Task<IActionResult> DownloadDump([FromRoute] Guid id, CancellationToken cancellationToken)
100100
{
101101
RequestTraceDetailsDto? details = await BuildDetailsQuery()
102102
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
@@ -144,8 +144,8 @@ public async Task<IActionResult> DownloadDump([FromRoute] long id, CancellationT
144144
return File(bytes, "application/octet-stream", $"request-trace-{id}.dump");
145145
}
146146

147-
[HttpGet("{id:long}/raw")]
148-
public async Task<IActionResult> DownloadRaw([FromRoute] long id, [FromQuery] string part, CancellationToken cancellationToken)
147+
[HttpGet("{id:guid}/raw")]
148+
public async Task<IActionResult> DownloadRaw([FromRoute] Guid id, [FromQuery] string part, CancellationToken cancellationToken)
149149
{
150150
RequestTracePayload? payload = await db.RequestTracePayloads
151151
.AsNoTracking()

src/BE/web/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public static async Task Main(string[] args)
151151

152152
app.UseAuthentication();
153153
app.UseAuthorization();
154-
app.UseMiddleware<RequestTraceMiddleware>();
154+
app.UseMiddleware<InboundRequestTraceMiddleware>();
155155
app.MapControllers();
156156
app.UseMiddleware<FrontendMiddleware>();
157157
app.UseStaticFiles();

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,17 @@ public interface IRequestTraceQueue
1717
IAsyncEnumerable<RequestTraceWriteModel> ReadAllAsync(CancellationToken cancellationToken);
1818

1919
long DroppedCount { get; }
20+
21+
long QueuedCount { get; }
22+
23+
long QueueHighWatermark { get; }
2024
}
2125

2226
public sealed class RequestTraceQueue : IRequestTraceQueue
2327
{
2428
private readonly Channel<RequestTraceWriteModel> _channel;
2529
private long _droppedCount;
30+
private long _queueHighWatermark;
2631

2732
public RequestTraceQueue()
2833
{
@@ -36,6 +41,10 @@ public RequestTraceQueue()
3641

3742
public long DroppedCount => Interlocked.Read(ref _droppedCount);
3843

44+
public long QueuedCount => _channel.Reader.CanCount ? _channel.Reader.Count : -1;
45+
46+
public long QueueHighWatermark => Interlocked.Read(ref _queueHighWatermark);
47+
3948
public bool TryEnqueueRequestHeader(RequestTraceRequestHeaderWriteModel item) => TryWrite(item);
4049

4150
public bool TryEnqueueRequestBody(RequestTraceRequestBodyWriteModel item) => TryWrite(item);
@@ -52,11 +61,31 @@ private bool TryWrite(RequestTraceWriteModel item)
5261
if (!written)
5362
{
5463
Interlocked.Increment(ref _droppedCount);
64+
return false;
5565
}
5666

57-
return written;
67+
long currentQueued = QueuedCount;
68+
UpdateHighWatermark(currentQueued);
69+
return true;
5870
}
5971

6072
public IAsyncEnumerable<RequestTraceWriteModel> ReadAllAsync(CancellationToken cancellationToken)
6173
=> _channel.Reader.ReadAllAsync(cancellationToken);
74+
75+
private void UpdateHighWatermark(long currentQueued)
76+
{
77+
while (true)
78+
{
79+
long snapshot = Interlocked.Read(ref _queueHighWatermark);
80+
if (currentQueued <= snapshot)
81+
{
82+
return;
83+
}
84+
85+
if (Interlocked.CompareExchange(ref _queueHighWatermark, currentQueued, snapshot) == snapshot)
86+
{
87+
return;
88+
}
89+
}
90+
}
6291
}

0 commit comments

Comments
 (0)