Skip to content

Commit 6319731

Browse files
committed
Enhance request logging with user context, update packages
1 parent daddeb5 commit 6319731

7 files changed

Lines changed: 118 additions & 18 deletions

File tree

Directory.Packages.props

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
<NoWarn>$(NoWarn);NU1507</NoWarn>
66
</PropertyGroup>
77
<ItemGroup>
8-
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.2.4" />
9-
<PackageVersion Include="AspNetCore.SecurityKey" Version="4.2.0" />
8+
<PackageVersion Include="AspNetCore.SecurityKey" Version="4.3.0" />
109
<PackageVersion Include="AssemblyMetadata.Generators" Version="2.2.0" />
1110
<PackageVersion Include="AutoMapper" Version="[14.0.0]" />
1211
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" />
@@ -25,7 +24,7 @@
2524
<PackageVersion Include="MailKit" Version="4.16.0" />
2625
<PackageVersion Include="MediatR" Version="[12.5.0]" />
2726
<PackageVersion Include="MessagePack" Version="3.1.4" />
28-
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.72" />
27+
<PackageVersion Include="Meziantou.Analyzer" Version="3.0.74" />
2928
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.7" />
3029
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="10.0.7" />
3130
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.7" />
@@ -72,7 +71,7 @@
7271
<PackageVersion Include="Testcontainers.MongoDb" Version="4.11.0" />
7372
<PackageVersion Include="Testcontainers.MsSql" Version="4.11.0" />
7473
<PackageVersion Include="TUnit" Version="1.43.11" />
75-
<PackageVersion Include="Twilio" Version="7.14.8" />
74+
<PackageVersion Include="Twilio" Version="7.14.9" />
7675
<PackageVersion Include="Verify.TUnit" Version="31.16.2" />
7776
<PackageVersion Include="YamlDotNet" Version="17.1.0" />
7877
</ItemGroup>

aspire.config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"appHost": {
3+
"path": "samples/EntityFramework/src/Tracker.Host/Tracker.Host.csproj"
4+
}
5+
}

samples/EntityFramework/src/Tracker.Host/Tracker.Host.csproj

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
2-
3-
<Sdk Name="Aspire.AppHost.Sdk" Version="13.0.0" />
1+
<Project Sdk="Aspire.AppHost.Sdk/13.3.0">
42

53
<PropertyGroup>
64
<OutputType>Exe</OutputType>
@@ -11,10 +9,6 @@
119
<UserSecretsId>ae704e9f-d42d-4e28-97ab-3459e3af48a9</UserSecretsId>
1210
</PropertyGroup>
1311

14-
<ItemGroup>
15-
<PackageReference Include="Aspire.Hosting.AppHost" />
16-
</ItemGroup>
17-
1812
<ItemGroup>
1913
<ProjectReference Include="..\Tracker.Service\Tracker.Service.csproj" />
2014
<ProjectReference Include="..\Tracker.Web\Tracker.Web.csproj" />

src/Arbiter.CommandQuery.Endpoints/RequestLoggingMiddleware.cs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Security.Claims;
23
using System.Text;
34

45
using Arbiter.Services;
@@ -59,15 +60,19 @@ public async Task InvokeAsync(HttpContext context)
5960
return;
6061
}
6162

62-
// track user identity in logs and tracing
63-
var userName = context.User?.Identity?.Name ?? "anonymous";
63+
var (UserName, UserId) = ResolveUserContext(context);
6464

65-
// enrich logs with user information
66-
using var scope = _logger.BeginScope("{UserName}", userName);
65+
// enrich logs with user information for downstream logs
66+
// use an array (IReadOnlyList<KeyValuePair<,>>) instead of Dictionary to avoid hashing/bucket allocation
67+
using var scope = _logger.BeginScope(new[]
68+
{
69+
new KeyValuePair<string, object?>("UserName", UserName),
70+
new KeyValuePair<string, object?>("UserId", UserId),
71+
});
6772

6873
// enrich tracing with user information
69-
Activity.Current?.SetTag("enduser.name", userName);
70-
74+
Activity.Current?.SetTag("enduser.name", UserName);
75+
Activity.Current?.SetTag("enduser.id", UserId);
7176

7277
// start timing
7378
var timestamp = Stopwatch.GetTimestamp();
@@ -203,6 +208,41 @@ private bool ShouldLogBody(HttpRequest request)
203208
}
204209
}
205210

211+
212+
private static (string UserName, string UserId) ResolveUserContext(HttpContext context)
213+
{
214+
var user = context.User;
215+
if (user?.Identity?.IsAuthenticated != true)
216+
return ("anonymous", "anonymous");
217+
218+
var userName = ResolveUserName(user);
219+
var userId = ResolveUserId(user);
220+
221+
return (userName, userId);
222+
}
223+
224+
private static string ResolveUserName(ClaimsPrincipal user)
225+
{
226+
var userName = user.Identity?.Name;
227+
if (!string.IsNullOrWhiteSpace(userName))
228+
return userName;
229+
230+
return user.FindFirstValue(ClaimTypes.Name)
231+
?? user.FindFirstValue(ClaimTypes.Upn)
232+
?? user.FindFirstValue(ClaimTypes.Email)
233+
?? user.FindFirstValue(ClaimTypes.NameIdentifier)
234+
?? "anonymous";
235+
}
236+
237+
private static string ResolveUserId(ClaimsPrincipal user)
238+
{
239+
return user.FindFirstValue(ClaimTypes.NameIdentifier)
240+
?? user.FindFirstValue("sub")
241+
?? user.FindFirstValue("oid")
242+
?? ResolveUserName(user);
243+
}
244+
245+
206246
[LoggerMessage(EventId = 1, Message = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms")]
207247
private static partial void LogRequestBasic(ILogger logger, LogLevel logLevel, string? requestMethod, string? requestPath, int statusCode, double elapsed);
208248

src/Arbiter.CommandQuery.Endpoints/RequestLoggingMiddlewareExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ public static class RequestLoggingMiddlewareExtensions
1717
/// <remarks>
1818
/// This middleware logs HTTP request details including method, path, status code, and elapsed time.
1919
/// Optionally, it can also log request bodies based on the configured options.
20+
/// Register this middleware after <c>UseAuthentication</c> so authenticated user context is available in the logging scope.
2021
/// </remarks>
2122
/// <example>
2223
/// <code>
24+
/// app.UseAuthentication();
25+
///
2326
/// app.UseRequestLogging(options =>
2427
/// {
2528
/// options.LogLevel = LogLevel.Debug;

src/Arbiter.CommandQuery.Endpoints/RequestLoggingOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@ public class RequestLoggingOptions
1414
/// <c>true</c> to include the request body; otherwise, <c>false</c>. Default is <c>false</c>.
1515
/// </value>
1616
/// <remarks>
17+
/// <para>
1718
/// Enabling request body logging may have a performance impact, especially for large payloads
1819
/// or high-traffic applications. Use <see cref="RequestBodyMaxSize"/> to limit the size of
1920
/// logged request bodies and <see cref="RequestBodyMimeTypes"/> to filter which content types are logged.
21+
/// </para>
22+
/// <para>
23+
/// Request bodies can contain sensitive data (for example credentials, tokens, or personal data),
24+
/// so enable this setting only when appropriate for your security and compliance requirements.
25+
/// </para>
2026
/// </remarks>
2127
public bool IncludeRequestBody { get; set; }
2228

test/Arbiter.CommandQuery.Endpoints.Tests/RequestLoggingMiddlewareTests.cs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Security.Claims;
12
using System.Text;
23
using Microsoft.AspNetCore.Http;
34
using Microsoft.Extensions.Logging;
@@ -79,6 +80,35 @@ public async Task InvokeAsync_DifferentHttpMethods_LogsCorrectMethod(string meth
7980
logger.LoggedMessages.Should().ContainMatch($"*{method}*{path}*");
8081
}
8182

83+
[Test]
84+
public async Task InvokeAsync_WhenUserIsAuthenticated_UsesAuthenticatedUserContext()
85+
{
86+
// Arrange
87+
var logger = new FakeLogger<RequestLoggingMiddleware>();
88+
var options = CreateOptions();
89+
var middleware = new RequestLoggingMiddleware(
90+
next: _ => Task.CompletedTask,
91+
logger: logger,
92+
options: options
93+
);
94+
95+
var context = CreateHttpContext("GET", "/api/test");
96+
var identity = new ClaimsIdentity(
97+
[
98+
new Claim(ClaimTypes.Name, "alice"),
99+
new Claim(ClaimTypes.NameIdentifier, "user-123")
100+
],
101+
authenticationType: "Test");
102+
context.User = new ClaimsPrincipal(identity);
103+
104+
// Act
105+
await middleware.InvokeAsync(context);
106+
107+
// Assert
108+
logger.LastUserName.Should().Be("alice");
109+
logger.LastUserId.Should().Be("user-123");
110+
}
111+
82112
#endregion
83113

84114
#region Response Status Code Tests
@@ -603,8 +633,22 @@ private class FakeLogger<T> : ILogger<T>
603633
{
604634
public List<string> LoggedMessages { get; } = new();
605635
public LogLevel LogLevel { get; private set; }
636+
public string? LastUserName { get; private set; }
637+
public string? LastUserId { get; private set; }
606638

607-
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
639+
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
640+
{
641+
if (state is IEnumerable<KeyValuePair<string, object?>> values)
642+
{
643+
var scopeValues = values.ToDictionary(x => x.Key, x => x.Value?.ToString(), StringComparer.Ordinal);
644+
scopeValues.TryGetValue("UserName", out var userName);
645+
scopeValues.TryGetValue("UserId", out var userId);
646+
LastUserName = userName;
647+
LastUserId = userId;
648+
}
649+
650+
return NullScope.Instance;
651+
}
608652

609653
public bool IsEnabled(LogLevel logLevel) => true;
610654

@@ -621,6 +665,15 @@ public void Log<TState>(
621665
}
622666
}
623667

668+
private sealed class NullScope : IDisposable
669+
{
670+
public static NullScope Instance { get; } = new();
671+
672+
public void Dispose()
673+
{
674+
}
675+
}
676+
624677
private class ThrowingStream : Stream
625678
{
626679
public override bool CanRead => true;

0 commit comments

Comments
 (0)