Skip to content

Commit f7a2d3c

Browse files
authored
Hoist activity fields to the logging scope (#11211)
- Expose SpanId, TraceId and ParentId to logging scope properties - Added tests to verify the Hierarchical ID format - Store the activity and lazily compute the various properties
1 parent 4235dd2 commit f7a2d3c

File tree

4 files changed

+101
-19
lines changed

4 files changed

+101
-19
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Text;
5+
6+
namespace Microsoft.AspNetCore.Hosting.Internal
7+
{
8+
/// <summary>
9+
/// Helpers for getting the right values from Activity no matter the format (w3c or hierarchical)
10+
/// </summary>
11+
internal static class ActivityExtensions
12+
{
13+
public static string GetSpanId(this Activity activity)
14+
{
15+
return activity.IdFormat switch
16+
{
17+
ActivityIdFormat.Hierarchical => activity.Id,
18+
ActivityIdFormat.W3C => activity.SpanId.ToHexString(),
19+
_ => null,
20+
} ?? string.Empty;
21+
}
22+
23+
public static string GetTraceId(this Activity activity)
24+
{
25+
return activity.IdFormat switch
26+
{
27+
ActivityIdFormat.Hierarchical => activity.RootId,
28+
ActivityIdFormat.W3C => activity.TraceId.ToHexString(),
29+
_ => null,
30+
} ?? string.Empty;
31+
}
32+
33+
public static string GetParentId(this Activity activity)
34+
{
35+
return activity.IdFormat switch
36+
{
37+
ActivityIdFormat.Hierarchical => activity.ParentId,
38+
ActivityIdFormat.W3C => activity.ParentSpanId.ToHexString(),
39+
_ => null,
40+
} ?? string.Empty;
41+
}
42+
}
43+
}

src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public void BeginRequest(HttpContext httpContext, ref HostingApplication.Context
6868
// Scope may be relevant for a different level of logging, so we always create it
6969
// see: https://github.com/aspnet/Hosting/pull/944
7070
// Scope can be null if logging is not on.
71-
context.Scope = _logger.RequestScope(httpContext, context.Activity.Id);
71+
context.Scope = _logger.RequestScope(httpContext, context.Activity);
7272

7373
if (_logger.IsEnabled(LogLevel.Information))
7474
{

src/Hosting/Hosting/src/Internal/HostingLoggerExtensions.cs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using System.Diagnostics;
78
using System.Globalization;
89
using System.Reflection;
910
using Microsoft.AspNetCore.Http;
@@ -13,9 +14,9 @@ namespace Microsoft.AspNetCore.Hosting.Internal
1314
{
1415
internal static class HostingLoggerExtensions
1516
{
16-
public static IDisposable RequestScope(this ILogger logger, HttpContext httpContext, string activityId)
17+
public static IDisposable RequestScope(this ILogger logger, HttpContext httpContext, Activity activity)
1718
{
18-
return logger.BeginScope(new HostingLogScope(httpContext, activityId));
19+
return logger.BeginScope(new HostingLogScope(httpContext, activity));
1920
}
2021

2122
public static void ApplicationError(this ILogger logger, Exception exception)
@@ -96,15 +97,15 @@ private class HostingLogScope : IReadOnlyList<KeyValuePair<string, object>>
9697
{
9798
private readonly string _path;
9899
private readonly string _traceIdentifier;
99-
private readonly string _activityId;
100+
private readonly Activity _activity;
100101

101102
private string _cachedToString;
102103

103104
public int Count
104105
{
105106
get
106107
{
107-
return 3;
108+
return 5;
108109
}
109110
}
110111

@@ -122,20 +123,29 @@ public KeyValuePair<string, object> this[int index]
122123
}
123124
else if (index == 2)
124125
{
125-
return new KeyValuePair<string, object>("ActivityId", _activityId);
126+
return new KeyValuePair<string, object>("SpanId", _activity.GetSpanId());
127+
}
128+
else if (index == 3)
129+
{
130+
return new KeyValuePair<string, object>("TraceId", _activity.GetTraceId());
131+
}
132+
else if (index == 4)
133+
{
134+
return new KeyValuePair<string, object>("ParentId", _activity.GetParentId());
126135
}
127136

128137
throw new ArgumentOutOfRangeException(nameof(index));
129138
}
130139
}
131140

132-
public HostingLogScope(HttpContext httpContext, string activityId)
141+
public HostingLogScope(HttpContext httpContext, Activity activity)
133142
{
134143
_traceIdentifier = httpContext.TraceIdentifier;
135144
_path = (httpContext.Request.PathBase.HasValue
136145
? httpContext.Request.PathBase + httpContext.Request.Path
137146
: httpContext.Request.Path).ToString();
138-
_activityId = activityId;
147+
148+
_activity = activity;
139149
}
140150

141151
public override string ToString()
@@ -144,10 +154,12 @@ public override string ToString()
144154
{
145155
_cachedToString = string.Format(
146156
CultureInfo.InvariantCulture,
147-
"RequestPath:{0} RequestId:{1}, ActivityId:{2}",
157+
"RequestPath:{0} RequestId:{1}, SpanId:{2}, TraceId:{3}, ParentId:{4}",
148158
_path,
149159
_traceIdentifier,
150-
_activityId);
160+
_activity.GetSpanId(),
161+
_activity.GetTraceId(),
162+
_activity.GetParentId());
151163
}
152164

153165
return _cachedToString;

src/Hosting/Hosting/test/HostingApplicationTests.cs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public void CreateContextWithDisabledLoggerDoesNotCreateActivity()
4242
}
4343

4444
[Fact]
45-
public void CreateContextWithEnabledLoggerCreatesActivityAndSetsActivityIdInScope()
45+
public void CreateContextWithEnabledLoggerCreatesActivityAndSetsActivityInScope()
4646
{
4747
// Arrange
4848
var logger = new LoggerWithScopes(isEnabled: true);
@@ -53,7 +53,36 @@ public void CreateContextWithEnabledLoggerCreatesActivityAndSetsActivityIdInScop
5353

5454
Assert.Single(logger.Scopes);
5555
var pairs = ((IReadOnlyList<KeyValuePair<string, object>>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value);
56-
Assert.Equal(Activity.Current.Id, pairs["ActivityId"].ToString());
56+
Assert.Equal(Activity.Current.Id, pairs["SpanId"].ToString());
57+
Assert.Equal(Activity.Current.RootId, pairs["TraceId"].ToString());
58+
Assert.Equal(string.Empty, pairs["ParentId"]?.ToString());
59+
}
60+
61+
[Fact]
62+
public void CreateContextWithEnabledLoggerAndRequestIdCreatesActivityAndSetsActivityInScope()
63+
{
64+
// Arrange
65+
66+
// Generate an id we can use for the request id header (in the correct format)
67+
var activity = new Activity("IncomingRequest");
68+
activity.Start();
69+
var id = activity.Id;
70+
activity.Stop();
71+
72+
var logger = new LoggerWithScopes(isEnabled: true);
73+
var hostingApplication = CreateApplication(out var features, logger: logger, configure: context =>
74+
{
75+
context.Request.Headers["Request-Id"] = id;
76+
});
77+
78+
// Act
79+
var context = hostingApplication.CreateContext(features);
80+
81+
Assert.Single(logger.Scopes);
82+
var pairs = ((IReadOnlyList<KeyValuePair<string, object>>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value);
83+
Assert.Equal(Activity.Current.Id, pairs["SpanId"].ToString());
84+
Assert.Equal(Activity.Current.RootId, pairs["TraceId"].ToString());
85+
Assert.Equal(id, pairs["ParentId"].ToString());
5786
}
5887

5988
[Fact]
@@ -90,10 +119,6 @@ public void ActivityStopDoesNotFireIfNoListenerAttachedForStart()
90119
// Act
91120
var context = hostingApplication.CreateContext(features);
92121

93-
Assert.Single(logger.Scopes);
94-
var pairs = ((IReadOnlyList<KeyValuePair<string, object>>)logger.Scopes[0]).ToDictionary(p => p.Key, p => p.Value);
95-
Assert.Equal(Activity.Current.Id, pairs["ActivityId"].ToString());
96-
97122
hostingApplication.DisposeContext(context, exception: null);
98123

99124
Assert.False(startFired);
@@ -398,13 +423,15 @@ private static void AssertProperty<T>(object o, string name)
398423
}
399424

400425
private static HostingApplication CreateApplication(out FeatureCollection features,
401-
DiagnosticListener diagnosticSource = null, ILogger logger = null)
426+
DiagnosticListener diagnosticSource = null, ILogger logger = null, Action<DefaultHttpContext> configure = null)
402427
{
403428
var httpContextFactory = new Mock<IHttpContextFactory>();
404429

405430
features = new FeatureCollection();
406431
features.Set<IHttpRequestFeature>(new HttpRequestFeature());
407-
httpContextFactory.Setup(s => s.Create(It.IsAny<IFeatureCollection>())).Returns(new DefaultHttpContext(features));
432+
var context = new DefaultHttpContext(features);
433+
configure?.Invoke(context);
434+
httpContextFactory.Setup(s => s.Create(It.IsAny<IFeatureCollection>())).Returns(context);
408435
httpContextFactory.Setup(s => s.Dispose(It.IsAny<HttpContext>()));
409436

410437
var hostingApplication = new HostingApplication(
@@ -453,7 +480,7 @@ public IDisposable BeginScope<TState>(TState state)
453480

454481
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
455482
{
456-
483+
457484
}
458485

459486
private class Scope : IDisposable

0 commit comments

Comments
 (0)