Skip to content

Commit 2117875

Browse files
committed
Propagate trace parent to SignalR hub invocations
1 parent 98ee502 commit 2117875

File tree

9 files changed

+394
-107
lines changed

9 files changed

+394
-107
lines changed

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

Lines changed: 10 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.AspNetCore.Http;
1010
using Microsoft.AspNetCore.Http.Features;
1111
using Microsoft.AspNetCore.Http.Metadata;
12+
using Microsoft.AspNetCore.Shared;
1213
using Microsoft.Extensions.Logging;
1314

1415
namespace Microsoft.AspNetCore.Hosting;
@@ -389,80 +390,23 @@ private void RecordRequestStartMetrics(HttpContext httpContext)
389390
hasDiagnosticListener = false;
390391

391392
var headers = httpContext.Request.Headers;
392-
_propagator.ExtractTraceIdAndState(headers,
393+
var activity = ActivityCreator.CreateActivity(
394+
_activitySource,
395+
_propagator,
396+
headers,
393397
static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
394398
{
395399
fieldValues = default;
396400
var headers = (IHeaderDictionary)carrier!;
397401
fieldValue = headers[fieldName];
398402
},
399-
out var requestId,
400-
out var traceState);
401-
402-
Activity? activity = null;
403-
if (_activitySource.HasListeners())
404-
{
405-
if (ActivityContext.TryParse(requestId, traceState, isRemote: true, out ActivityContext context))
406-
{
407-
// The requestId used the W3C ID format. Unfortunately, the ActivitySource.CreateActivity overload that
408-
// takes a string parentId never sets HasRemoteParent to true. We work around that by calling the
409-
// ActivityContext overload instead which sets HasRemoteParent to parentContext.IsRemote.
410-
// https://github.com/dotnet/aspnetcore/pull/41568#discussion_r868733305
411-
activity = _activitySource.CreateActivity(ActivityName, ActivityKind.Server, context);
412-
}
413-
else
414-
{
415-
// Pass in the ID we got from the headers if there was one.
416-
activity = _activitySource.CreateActivity(ActivityName, ActivityKind.Server, string.IsNullOrEmpty(requestId) ? null! : requestId);
417-
}
418-
}
419-
403+
ActivityName,
404+
tags: null,
405+
links: null,
406+
loggingEnabled || diagnosticListenerActivityCreationEnabled);
420407
if (activity is null)
421408
{
422-
// CreateActivity didn't create an Activity (this is an optimization for the
423-
// case when there are no listeners). Let's create it here if needed.
424-
if (loggingEnabled || diagnosticListenerActivityCreationEnabled)
425-
{
426-
activity = new Activity(ActivityName);
427-
if (!string.IsNullOrEmpty(requestId))
428-
{
429-
activity.SetParentId(requestId);
430-
}
431-
}
432-
else
433-
{
434-
return null;
435-
}
436-
}
437-
438-
// The trace id was successfully extracted, so we can set the trace state
439-
// https://www.w3.org/TR/trace-context/#tracestate-header
440-
if (!string.IsNullOrEmpty(requestId))
441-
{
442-
if (!string.IsNullOrEmpty(traceState))
443-
{
444-
activity.TraceStateString = traceState;
445-
}
446-
}
447-
448-
// Baggage can be used regardless of whether a distributed trace id was present on the inbound request.
449-
// https://www.w3.org/TR/baggage/#abstract
450-
var baggage = _propagator.ExtractBaggage(headers, static (object? carrier, string fieldName, out string? fieldValue, out IEnumerable<string>? fieldValues) =>
451-
{
452-
fieldValues = default;
453-
var headers = (IHeaderDictionary)carrier!;
454-
fieldValue = headers[fieldName];
455-
});
456-
457-
// AddBaggage adds items at the beginning of the list, so we need to add them in reverse to keep the same order as the client
458-
// By contract, the propagator has already reversed the order of items so we need not reverse it again
459-
// Order could be important if baggage has two items with the same key (that is allowed by the contract)
460-
if (baggage is not null)
461-
{
462-
foreach (var baggageItem in baggage)
463-
{
464-
activity.AddBaggage(baggageItem.Key, baggageItem.Value);
465-
}
409+
return null;
466410
}
467411

468412
_diagnosticListener.OnActivityImport(activity, httpContext);

src/Hosting/Hosting/src/Microsoft.AspNetCore.Hosting.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Compile Include="$(SharedSourceRoot)StaticWebAssets\**\*.cs" LinkBase="StaticWebAssets" />
1919
<Compile Include="$(SharedSourceRoot)Metrics\MetricsExtensions.cs" />
2020
<Compile Include="$(SharedSourceRoot)Metrics\MetricsConstants.cs" />
21+
<Compile Include="$(SharedSourceRoot)Diagnostics\ActivityCreator.cs" />
2122
</ItemGroup>
2223

2324
<ItemGroup>
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
6+
namespace Microsoft.AspNetCore.Shared;
7+
8+
internal static class ActivityCreator
9+
{
10+
/// <summary>
11+
/// Create an activity with details received from a remote source.
12+
/// </summary>
13+
public static Activity? CreateActivity(
14+
ActivitySource activitySource,
15+
DistributedContextPropagator propagator,
16+
object? distributedContextCarrier,
17+
DistributedContextPropagator.PropagatorGetterCallback propagatorGetter,
18+
string activityName,
19+
IEnumerable<KeyValuePair<string, object?>>? tags,
20+
IEnumerable<ActivityLink>? links,
21+
bool diagnosticsOrLoggingEnabled)
22+
{
23+
string? requestId = null;
24+
string? traceState = null;
25+
if (distributedContextCarrier != null)
26+
{
27+
propagator.ExtractTraceIdAndState(
28+
distributedContextCarrier,
29+
propagatorGetter,
30+
out requestId,
31+
out traceState);
32+
}
33+
34+
Activity? activity = null;
35+
if (activitySource.HasListeners())
36+
{
37+
if (ActivityContext.TryParse(requestId, traceState, isRemote: true, out ActivityContext context))
38+
{
39+
// The requestId used the W3C ID format. Unfortunately, the ActivitySource.CreateActivity overload that
40+
// takes a string parentId never sets HasRemoteParent to true. We work around that by calling the
41+
// ActivityContext overload instead which sets HasRemoteParent to parentContext.IsRemote.
42+
// https://github.com/dotnet/aspnetcore/pull/41568#discussion_r868733305
43+
activity = activitySource.CreateActivity(activityName, ActivityKind.Server, context, tags: tags, links: links);
44+
}
45+
else
46+
{
47+
// Pass in the ID we got from the headers if there was one.
48+
activity = activitySource.CreateActivity(activityName, ActivityKind.Server, string.IsNullOrEmpty(requestId) ? null! : requestId, tags: tags, links: links);
49+
}
50+
}
51+
52+
if (activity is null)
53+
{
54+
// CreateActivity didn't create an Activity (this is an optimization for the
55+
// case when there are no listeners). Let's create it here if needed.
56+
if (diagnosticsOrLoggingEnabled)
57+
{
58+
activity = new Activity(activityName);
59+
if (!string.IsNullOrEmpty(requestId))
60+
{
61+
activity.SetParentId(requestId);
62+
}
63+
if (tags != null)
64+
{
65+
foreach (var tag in tags)
66+
{
67+
activity.AddTag(tag.Key, tag.Value);
68+
}
69+
}
70+
if (links != null)
71+
{
72+
foreach (var link in links)
73+
{
74+
activity.AddLink(link);
75+
}
76+
}
77+
}
78+
else
79+
{
80+
return null;
81+
}
82+
}
83+
84+
// The trace id was successfully extracted, so we can set the trace state
85+
// https://www.w3.org/TR/trace-context/#tracestate-header
86+
if (!string.IsNullOrEmpty(requestId))
87+
{
88+
if (!string.IsNullOrEmpty(traceState))
89+
{
90+
activity.TraceStateString = traceState;
91+
}
92+
}
93+
94+
// Baggage can be used regardless of whether a distributed trace id was present on the inbound request.
95+
// https://www.w3.org/TR/baggage/#abstract
96+
if (distributedContextCarrier != null)
97+
{
98+
var baggage = propagator.ExtractBaggage(distributedContextCarrier, propagatorGetter);
99+
100+
// AddBaggage adds items at the beginning of the list, so we need to add them in reverse to keep the same order as the client
101+
// By contract, the propagator has already reversed the order of items so we need not reverse it again
102+
// Order could be important if baggage has two items with the same key (that is allowed by the contract)
103+
if (baggage is not null)
104+
{
105+
foreach (var baggageItem in baggage)
106+
{
107+
activity.AddBaggage(baggageItem.Key, baggageItem.Value);
108+
}
109+
}
110+
}
111+
112+
return activity;
113+
}
114+
}

src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,7 @@ private async Task InvokeCore(ConnectionState connectionState, string methodName
10371037

10381038
// Client invocations are always blocking
10391039
var invocationMessage = new InvocationMessage(irq.InvocationId, methodName, args, streams);
1040+
InjectHeaders(invocationMessage);
10401041

10411042
Log.RegisteringInvocation(_logger, irq.InvocationId);
10421043
connectionState.AddInvocation(irq);
@@ -1063,6 +1064,7 @@ private async Task InvokeStreamCore(ConnectionState connectionState, string meth
10631064
Log.PreparingStreamingInvocation(_logger, irq.InvocationId, methodName, irq.ResultType.FullName!, args.Length);
10641065

10651066
var invocationMessage = new StreamInvocationMessage(irq.InvocationId, methodName, args, streams);
1067+
InjectHeaders(invocationMessage);
10661068

10671069
Log.RegisteringInvocation(_logger, irq.InvocationId);
10681070

@@ -1083,6 +1085,21 @@ private async Task InvokeStreamCore(ConnectionState connectionState, string meth
10831085
}
10841086
}
10851087

1088+
private static void InjectHeaders(HubInvocationMessage invocationMessage)
1089+
{
1090+
if (Activity.Current is { } currentActivity)
1091+
{
1092+
DistributedContextPropagator.Current.Inject(currentActivity, invocationMessage, static (carrier, key, value) =>
1093+
{
1094+
if (carrier is HubInvocationMessage invocationMessage)
1095+
{
1096+
invocationMessage.Headers ??= new Dictionary<string, string>();
1097+
invocationMessage.Headers[key] = value;
1098+
}
1099+
});
1100+
}
1101+
}
1102+
10861103
private async Task SendHubMessage(ConnectionState connectionState, HubMessage hubMessage, CancellationToken cancellationToken = default)
10871104
{
10881105
_state.AssertConnectionValid();

src/SignalR/common/testassets/Tests.Utils/TestClient.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using Microsoft.AspNetCore.Connections.Features;
99
using Microsoft.AspNetCore.Internal;
1010
using Microsoft.AspNetCore.SignalR.Protocol;
11+
using System.Reflection.PortableExecutable;
12+
1113
#if TESTUTILS
1214
using Microsoft.AspNetCore.InternalTesting;
1315
#else
@@ -99,6 +101,12 @@ public async Task<IList<HubMessage>> StreamAsync(string methodName, string[] str
99101
return await ListenAllAsync(invocationId);
100102
}
101103

104+
public async Task<IList<HubMessage>> StreamAsync(string methodName, string[] streamIds, IDictionary<string, string> headers, params object[] args)
105+
{
106+
var invocationId = await SendStreamInvocationAsync(methodName, streamIds, headers, args);
107+
return await ListenAllAsync(invocationId);
108+
}
109+
102110
public async Task<IList<HubMessage>> ListenAllAsync(string invocationId)
103111
{
104112
var result = new List<HubMessage>();
@@ -185,10 +193,20 @@ public Task<string> SendInvocationAsync(string methodName, params object[] args)
185193
return SendInvocationAsync(methodName, nonBlocking: false, args: args);
186194
}
187195

196+
public Task<string> SendInvocationAsync(string methodName, IDictionary<string, string> headers, params object[] args)
197+
{
198+
return SendInvocationAsync(methodName, nonBlocking: false, headers: headers, args: args);
199+
}
200+
188201
public Task<string> SendInvocationAsync(string methodName, bool nonBlocking, params object[] args)
202+
{
203+
return SendInvocationAsync(methodName, nonBlocking: nonBlocking, headers: null, args: args);
204+
}
205+
206+
public Task<string> SendInvocationAsync(string methodName, bool nonBlocking, IDictionary<string, string> headers, params object[] args)
189207
{
190208
var invocationId = nonBlocking ? null : GetInvocationId();
191-
return SendHubMessageAsync(new InvocationMessage(invocationId, methodName, args));
209+
return SendHubMessageAsync(new InvocationMessage(invocationId, methodName, args) { Headers = headers });
192210
}
193211

194212
public Task<string> SendStreamInvocationAsync(string methodName, params object[] args)
@@ -197,9 +215,14 @@ public Task<string> SendStreamInvocationAsync(string methodName, params object[]
197215
}
198216

199217
public Task<string> SendStreamInvocationAsync(string methodName, string[] streamIds, params object[] args)
218+
{
219+
return SendStreamInvocationAsync(methodName, streamIds: streamIds, headers: null, args);
220+
}
221+
222+
public Task<string> SendStreamInvocationAsync(string methodName, string[] streamIds, IDictionary<string, string> headers, params object[] args)
200223
{
201224
var invocationId = GetInvocationId();
202-
return SendHubMessageAsync(new StreamInvocationMessage(invocationId, methodName, args, streamIds));
225+
return SendHubMessageAsync(new StreamInvocationMessage(invocationId, methodName, args, streamIds) { Headers = headers });
203226
}
204227

205228
public Task<string> BeginUploadStreamAsync(string invocationId, string methodName, string[] streamIds, params object[] args)

src/SignalR/server/Core/src/HubConnectionHandler.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,6 @@ public override async Task OnConnectedAsync(ConnectionContext connection)
136136
OriginalActivity = Activity.Current,
137137
};
138138

139-
// Get off the parent span.
140-
// This is likely the Http Request span and we want Hub method invocations to not be collected under a long running span.
141-
Activity.Current = null;
142-
143139
var resolvedSupportedProtocols = (supportedProtocols as IReadOnlyList<string>) ?? supportedProtocols.ToList();
144140
if (!await connectionContext.HandshakeAsync(handshakeTimeout, resolvedSupportedProtocols, _protocolResolver, _userIdProvider, _enableDetailedErrors))
145141
{

0 commit comments

Comments
 (0)