Skip to content

Commit 5083488

Browse files
Fix distributed tracing in ASP.NET Core MVC (DataDog#303)
* add logging * use DictionaryHeadersCollection to extract distributed tracing headers * emit IL instead of using `dynamic`
1 parent 06c8ef9 commit 5083488

File tree

2 files changed

+135
-29
lines changed

2 files changed

+135
-29
lines changed

src/Datadog.Trace.ClrProfiler.Managed/Integrations/AspNetCoreMvc2Integration.cs

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Reflection;
45
using System.Text;
6+
using Datadog.Trace.Headers;
7+
using Datadog.Trace.Logging;
58

69
namespace Datadog.Trace.ClrProfiler.Integrations
710
{
@@ -13,6 +16,8 @@ public sealed class AspNetCoreMvc2Integration : IDisposable
1316
internal const string OperationName = "aspnet-coremvc.request";
1417
private const string HttpContextKey = "__Datadog.Trace.ClrProfiler.Integrations." + nameof(AspNetCoreMvc2Integration);
1518

19+
private static readonly ILog Log = LogProvider.GetLogger(typeof(AspNetCoreMvc2Integration));
20+
1621
private static Action<object, object, object, object> _beforeAction;
1722
private static Action<object, object, object, object> _afterAction;
1823

@@ -36,7 +41,33 @@ public AspNetCoreMvc2Integration(object actionDescriptorObj, object httpContextO
3641
string httpMethod = _httpContext.Request.Method.ToUpperInvariant();
3742
string url = GetDisplayUrl(_httpContext.Request).ToLowerInvariant();
3843

39-
_scope = Tracer.Instance.StartActive(OperationName);
44+
SpanContext propagatedContext = null;
45+
46+
if (Tracer.Instance.ActiveScope == null)
47+
{
48+
try
49+
{
50+
// extract propagated http headers
51+
IEnumerable requestHeaders = _httpContext.Request.Headers;
52+
int headerCount = _httpContext.Request.Headers.Count;
53+
var headersCollection = new DictionaryHeadersCollection(headerCount);
54+
55+
foreach (dynamic header in requestHeaders)
56+
{
57+
string key = header.Key;
58+
string[] values = header.Value.ToArray();
59+
headersCollection.Add(key, values);
60+
}
61+
62+
propagatedContext = SpanContextPropagator.Instance.Extract(headersCollection);
63+
}
64+
catch (Exception ex)
65+
{
66+
Log.ErrorException("Error extracting propagated HTTP headers.", ex);
67+
}
68+
}
69+
70+
_scope = Tracer.Instance.StartActive(OperationName, propagatedContext);
4071
var span = _scope.Span;
4172
span.Type = SpanTypes.Web;
4273
span.ResourceName = $"{httpMethod} {controllerName}.{actionName}";
@@ -45,9 +76,10 @@ public AspNetCoreMvc2Integration(object actionDescriptorObj, object httpContextO
4576
span.SetTag(Tags.AspNetController, controllerName);
4677
span.SetTag(Tags.AspNetAction, actionName);
4778
}
48-
catch
79+
catch (Exception) when (DisposeObject(_scope))
4980
{
50-
// TODO: logging
81+
// unreachable code
82+
throw;
5183
}
5284
}
5385

@@ -63,22 +95,25 @@ public AspNetCoreMvc2Integration(object actionDescriptorObj, object httpContextO
6395
TargetAssembly = "Microsoft.AspNetCore.Mvc.Core",
6496
TargetType = "Microsoft.AspNetCore.Mvc.Internal.MvcCoreDiagnosticSourceExtensions")]
6597
public static void BeforeAction(
66-
object diagnosticSource,
67-
object actionDescriptor,
68-
dynamic httpContext,
69-
object routeData)
98+
object diagnosticSource,
99+
object actionDescriptor,
100+
object httpContext,
101+
object routeData)
70102
{
71103
AspNetCoreMvc2Integration integration = null;
72104

73105
try
74106
{
75107
integration = new AspNetCoreMvc2Integration(actionDescriptor, httpContext);
76-
IDictionary<object, object> contextItems = httpContext.Items;
77-
contextItems[HttpContextKey] = integration;
108+
109+
if (httpContext.TryGetPropertyValue("Items", out IDictionary<object, object> contextItems))
110+
{
111+
contextItems[HttpContextKey] = integration;
112+
}
78113
}
79-
catch
114+
catch (Exception ex)
80115
{
81-
// TODO: log this as an instrumentation error, but continue calling instrumented method
116+
Log.ErrorExceptionForFilter($"Error creating {nameof(AspNetCoreMvc2Integration)}.", ex);
82117
}
83118

84119
try
@@ -89,25 +124,24 @@ public static void BeforeAction(
89124
var type = assembly.GetType("Microsoft.AspNetCore.Mvc.Internal.MvcCoreDiagnosticSourceExtensions");
90125

91126
_beforeAction = DynamicMethodBuilder<Action<object, object, object, object>>.CreateMethodCallDelegate(
92-
type,
93-
"BeforeAction");
127+
type,
128+
"BeforeAction");
94129
}
95130
}
96-
catch
131+
catch (Exception ex)
97132
{
98-
// TODO: log this as an instrumentation error, we cannot call instrumented method,
99133
// profiled app will continue working without DiagnosticSource
134+
Log.ErrorException("Error calling Microsoft.AspNetCore.Mvc.Internal.MvcCoreDiagnosticSourceExtensions.BeforeAction()", ex);
100135
}
101136

102137
try
103138
{
104139
// call the original method, catching and rethrowing any unhandled exceptions
105140
_beforeAction?.Invoke(diagnosticSource, actionDescriptor, httpContext, routeData);
106141
}
107-
catch (Exception ex)
142+
catch (Exception ex) when (integration?.SetException(ex) ?? false)
108143
{
109-
integration?.SetException(ex);
110-
144+
// unreachable code
111145
throw;
112146
}
113147
}
@@ -124,21 +158,23 @@ public static void BeforeAction(
124158
TargetAssembly = "Microsoft.AspNetCore.Mvc.Core",
125159
TargetType = "Microsoft.AspNetCore.Mvc.Internal.MvcCoreDiagnosticSourceExtensions")]
126160
public static void AfterAction(
127-
object diagnosticSource,
128-
object actionDescriptor,
129-
dynamic httpContext,
130-
object routeData)
161+
object diagnosticSource,
162+
object actionDescriptor,
163+
object httpContext,
164+
object routeData)
131165
{
132166
AspNetCoreMvc2Integration integration = null;
133167

134168
try
135169
{
136-
IDictionary<object, object> contextItems = httpContext?.Items;
137-
integration = contextItems?[HttpContextKey] as AspNetCoreMvc2Integration;
170+
if (httpContext.TryGetPropertyValue("Items", out IDictionary<object, object> contextItems))
171+
{
172+
integration = contextItems?[HttpContextKey] as AspNetCoreMvc2Integration;
173+
}
138174
}
139-
catch
175+
catch (Exception ex)
140176
{
141-
// TODO: log this as an instrumentation error, but continue calling instrumented method
177+
Log.ErrorExceptionForFilter($"Error accessing {nameof(AspNetCoreMvc2Integration)}.", ex);
142178
}
143179

144180
try
@@ -148,8 +184,8 @@ public static void AfterAction(
148184
var type = actionDescriptor.GetType().Assembly.GetType("Microsoft.AspNetCore.Mvc.Internal.MvcCoreDiagnosticSourceExtensions");
149185

150186
_afterAction = DynamicMethodBuilder<Action<object, object, object, object>>.CreateMethodCallDelegate(
151-
type,
152-
"AfterAction");
187+
type,
188+
"AfterAction");
153189
}
154190
}
155191
catch
@@ -179,9 +215,11 @@ public static void AfterAction(
179215
/// Tags the current span as an error. Called when an unhandled exception is thrown in the instrumented method.
180216
/// </summary>
181217
/// <param name="ex">The exception that was thrown and not handled in the instrumented method.</param>
182-
public void SetException(Exception ex)
218+
/// <returns>Always <c>false</c>.</returns>
219+
public bool SetException(Exception ex)
183220
{
184221
_scope?.Span?.SetException(ex);
222+
return false;
185223
}
186224

187225
/// <summary>
@@ -217,5 +255,11 @@ private static string GetDisplayUrl(dynamic request)
217255
.Append(queryString)
218256
.ToString();
219257
}
258+
259+
private bool DisposeObject(IDisposable disposable)
260+
{
261+
disposable?.Dispose();
262+
return false;
263+
}
220264
}
221265
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Datadog.Trace.Headers
6+
{
7+
internal class DictionaryHeadersCollection : IHeadersCollection
8+
{
9+
private readonly IDictionary<string, IList<string>> _headers;
10+
11+
public DictionaryHeadersCollection()
12+
{
13+
_headers = new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
14+
}
15+
16+
public DictionaryHeadersCollection(int capacity)
17+
{
18+
_headers = new Dictionary<string, IList<string>>(capacity, StringComparer.OrdinalIgnoreCase);
19+
}
20+
21+
public DictionaryHeadersCollection(IDictionary<string, IList<string>> dictionary)
22+
{
23+
_headers = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
24+
}
25+
26+
public IEnumerable<string> GetValues(string name)
27+
{
28+
return _headers.TryGetValue(name, out var values)
29+
? values
30+
: Enumerable.Empty<string>();
31+
}
32+
33+
public void Set(string name, string value)
34+
{
35+
_headers.Remove(name);
36+
_headers.Add(name, new List<string> { value });
37+
}
38+
39+
public void Add(string name, string value)
40+
{
41+
Add(name, new[] { value });
42+
}
43+
44+
public void Add(string name, IEnumerable<string> values)
45+
{
46+
if (!_headers.TryGetValue(name, out var list))
47+
{
48+
list = new List<string>();
49+
}
50+
51+
foreach (var value in values)
52+
{
53+
list.Add(value);
54+
}
55+
}
56+
57+
public void Remove(string name)
58+
{
59+
_headers.Remove(name);
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)