Skip to content

Commit 899add0

Browse files
Distributed tracing: ASP.NET MVC & Web API (DataDog#276)
* add missing logging, using exception filters where possible * refactor AspNetMvcIntegration a little to follow patterns of newer integrations * read distributed tracing values from http headers * make it public like other integrations * fix operation name * ensure span is always disposed, log exceptions, using filters where possible * fix comments * add project dependency so sample app is built when running integration test * revert parameters types to dynamic because it breaks dynamic method invocation; fix xml doc comments * don't trace the http request made by the integration test itself * don't try to run IIS tests at the same time * move `OperationName` and `ServiceName` to where they are used
1 parent c40bb60 commit 899add0

File tree

9 files changed

+130
-133
lines changed

9 files changed

+130
-133
lines changed

Datadog.Trace.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples.ExampleLibraryTrace
8383
EndProject
8484
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Datadog.Trace.ClrProfiler.IntegrationTests", "test\Datadog.Trace.ClrProfiler.IntegrationTests\Datadog.Trace.ClrProfiler.IntegrationTests.csproj", "{0D546118-B70A-44D0-B675-39EDB99FCEEE}"
8585
ProjectSection(ProjectDependencies) = postProject
86+
{3C6DD42E-9214-4747-92BA-78DE29AACE59} = {3C6DD42E-9214-4747-92BA-78DE29AACE59}
8687
{9951A03A-AD1B-48D8-A96D-B85532BEDADD} = {9951A03A-AD1B-48D8-A96D-B85532BEDADD}
8788
{3493346B-44F6-4F50-8FB4-51D0090DF544} = {3493346B-44F6-4F50-8FB4-51D0090DF544}
8889
{C0C8D381-D6B9-4C76-9428-F40F2FA93A9A} = {C0C8D381-D6B9-4C76-9428-F40F2FA93A9A}

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

Lines changed: 50 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -5,55 +5,51 @@
55
using System.Web;
66
using System.Web.Routing;
77
using Datadog.Trace.ExtensionMethods;
8+
using Datadog.Trace.Logging;
89

910
namespace Datadog.Trace.ClrProfiler.Integrations
1011
{
1112
/// <summary>
1213
/// The ASP.NET MVC integration.
1314
/// </summary>
14-
public sealed class AspNetMvcIntegration : IDisposable
15+
public static class AspNetMvcIntegration
1516
{
1617
internal const string OperationName = "aspnet-mvc.request";
1718
private const string HttpContextKey = "__Datadog.Trace.ClrProfiler.Integrations.AspNetMvcIntegration";
1819

1920
private static readonly Type ControllerContextType = Type.GetType("System.Web.Mvc.ControllerContext, System.Web.Mvc", throwOnError: false);
2021
private static readonly Type RouteCollectionRouteType = Type.GetType("System.Web.Mvc.Routing.RouteCollectionRoute, System.Web.Mvc", throwOnError: false);
21-
22-
private readonly HttpContextBase _httpContext;
23-
private readonly Scope _scope;
22+
private static readonly ILog Log = LogProvider.GetLogger(typeof(AspNetMvcIntegration));
2423

2524
/// <summary>
26-
/// Initializes a new instance of the <see cref="AspNetMvcIntegration"/> class.
25+
/// Creates a scope used to instrument an MVC action and populates some common details.
2726
/// </summary>
28-
/// <param name="controllerContextObj">The System.Web.Mvc.ControllerContext that was passed as an argument to the instrumented method.</param>
29-
public AspNetMvcIntegration(object controllerContextObj)
27+
/// <param name="controllerContext">The System.Web.Mvc.ControllerContext that was passed as an argument to the instrumented method.</param>
28+
/// <returns>A new scope used to instrument an MVC action.</returns>
29+
public static Scope CreateScope(dynamic controllerContext)
3030
{
31-
if (controllerContextObj == null || ControllerContextType == null)
31+
if (ControllerContextType == null ||
32+
controllerContext == null ||
33+
((object)controllerContext)?.GetType() != ControllerContextType)
3234
{
3335
// bail out early
34-
return;
36+
return null;
3537
}
3638

39+
Scope scope = null;
40+
3741
try
3842
{
39-
if (controllerContextObj.GetType() != ControllerContextType)
40-
{
41-
return;
42-
}
43-
44-
// access the controller context without referencing System.Web.Mvc directly
45-
dynamic controllerContext = controllerContextObj;
43+
var httpContext = controllerContext.HttpContext as HttpContextBase;
4644

47-
_httpContext = controllerContext.HttpContext;
48-
49-
if (_httpContext == null)
45+
if (httpContext == null)
5046
{
51-
return;
47+
return null;
5248
}
5349

54-
string host = _httpContext.Request.Headers.Get("Host");
55-
string httpMethod = _httpContext.Request.HttpMethod.ToUpperInvariant();
56-
string url = _httpContext.Request.RawUrl.ToLowerInvariant();
50+
string host = httpContext.Request.Headers.Get("Host");
51+
string httpMethod = httpContext.Request.HttpMethod.ToUpperInvariant();
52+
string url = httpContext.Request.RawUrl.ToLowerInvariant();
5753

5854
RouteData routeData = controllerContext.RouteData as RouteData;
5955
Route route = routeData?.Route as Route;
@@ -76,8 +72,11 @@ public AspNetMvcIntegration(object controllerContextObj)
7672
string actionName = (routeValues?.GetValueOrDefault("action") as string)?.ToLowerInvariant();
7773
string resourceName = $"{httpMethod} {controllerName}.{actionName}";
7874

79-
_scope = Tracer.Instance.StartActive(OperationName);
80-
Span span = _scope.Span;
75+
// extract distributed tracing values
76+
var spanContext = httpContext.Request.Headers.Extract();
77+
78+
scope = Tracer.Instance.StartActive(OperationName, spanContext);
79+
Span span = scope.Span;
8180
span.Type = SpanTypes.Web;
8281
span.ResourceName = resourceName;
8382
span.SetTag(Tags.HttpRequestHeadersHost, host);
@@ -87,16 +86,18 @@ public AspNetMvcIntegration(object controllerContextObj)
8786
span.SetTag(Tags.AspNetController, controllerName);
8887
span.SetTag(Tags.AspNetAction, actionName);
8988
}
90-
catch
89+
catch (Exception ex)
9190
{
92-
// TODO: logging
91+
Log.ErrorException("Error creating or populating scope.", ex);
9392
}
93+
94+
return scope;
9495
}
9596

9697
/// <summary>
97-
/// Wrapper method used to instrument System.Web.Mvc.Async.AsyncControllerActionInvoker.BeginInvokeAction().
98+
/// Wrapper method used to instrument System.Web.Mvc.Async.IAsyncActionInvoker.BeginInvokeAction().
9899
/// </summary>
99-
/// <param name="asyncControllerActionInvoker">The AsyncControllerActionInvoker instance.</param>
100+
/// <param name="asyncControllerActionInvoker">The IAsyncActionInvoker instance.</param>
100101
/// <param name="controllerContext">The ControllerContext for the current request.</param>
101102
/// <param name="actionName">The name of the controller action.</param>
102103
/// <param name="callback">An <see cref="AsyncCallback"/> delegate.</param>
@@ -113,37 +114,37 @@ public static object BeginInvokeAction(
113114
dynamic callback,
114115
dynamic state)
115116
{
116-
AspNetMvcIntegration integration = null;
117+
Scope scope = null;
117118

118119
try
119120
{
120121
if (HttpContext.Current != null)
121122
{
122-
integration = new AspNetMvcIntegration((object)controllerContext);
123-
HttpContext.Current.Items[HttpContextKey] = integration;
123+
scope = CreateScope(controllerContext);
124+
HttpContext.Current.Items[HttpContextKey] = scope;
124125
}
125126
}
126-
catch
127+
catch (Exception ex)
127128
{
128-
// TODO: log this as an instrumentation error, but continue calling instrumented method
129+
Log.ErrorException("Error instrumenting method {0}", ex, "System.Web.Mvc.Async.IAsyncActionInvoker.BeginInvokeAction()");
129130
}
130131

131132
try
132133
{
133-
// call the original method, catching and rethrowing any unhandled exceptions
134+
// call the original method, inspecting (but not catching) any unhandled exceptions
134135
return asyncControllerActionInvoker.BeginInvokeAction(controllerContext, actionName, callback, state);
135136
}
136-
catch (Exception ex)
137+
catch (Exception ex) when (scope?.Span.SetExceptionForFilter(ex) ?? false)
137138
{
138-
integration?.SetException(ex);
139+
// unreachable code
139140
throw;
140141
}
141142
}
142143

143144
/// <summary>
144-
/// Wrapper method used to instrument System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeAction().
145+
/// Wrapper method used to instrument System.Web.Mvc.Async.IAsyncActionInvoker.EndInvokeAction().
145146
/// </summary>
146-
/// <param name="asyncControllerActionInvoker">The AsyncControllerActionInvoker instance.</param>
147+
/// <param name="asyncControllerActionInvoker">The IAsyncActionInvoker instance.</param>
147148
/// <param name="asyncResult">The <see cref="IAsyncResult"/> returned by <see cref="BeginInvokeAction"/>.</param>
148149
/// <returns>Returns the <see cref="bool"/> returned by the original EndInvokeAction().</returns>
149150
[InterceptMethod(
@@ -152,66 +153,31 @@ public static object BeginInvokeAction(
152153
TargetType = "System.Web.Mvc.Async.IAsyncActionInvoker")]
153154
public static bool EndInvokeAction(dynamic asyncControllerActionInvoker, dynamic asyncResult)
154155
{
155-
AspNetMvcIntegration integration = null;
156+
Scope scope = null;
157+
var httpContext = HttpContext.Current;
156158

157159
try
158160
{
159-
if (HttpContext.Current != null)
160-
{
161-
integration = HttpContext.Current?.Items[HttpContextKey] as AspNetMvcIntegration;
162-
}
161+
scope = httpContext?.Items[HttpContextKey] as Scope;
163162
}
164-
catch
163+
catch (Exception ex)
165164
{
166-
// TODO: log this as an instrumentation error, but continue calling instrumented method
165+
Log.ErrorException("Error instrumenting method {0}", ex, "System.Web.Mvc.Async.IAsyncActionInvoker.EndInvokeAction()");
167166
}
168167

169168
try
170169
{
171-
// call the original method, catching and rethrowing any unhandled exceptions
172-
return asyncControllerActionInvoker.EndInvokeAction(asyncResult);
170+
// call the original method, inspecting (but not catching) any unhandled exceptions
171+
return (bool)asyncControllerActionInvoker.EndInvokeAction(asyncResult);
173172
}
174-
catch (Exception ex)
173+
catch (Exception ex) when (scope?.Span.SetExceptionForFilter(ex) ?? false)
175174
{
176-
integration?.SetException(ex);
175+
// unreachable code
177176
throw;
178177
}
179178
finally
180179
{
181-
integration?.Dispose();
182-
}
183-
}
184-
185-
/// <summary>
186-
/// Tags the current span as an error. Called when an unhandled exception is thrown in the instrumented method.
187-
/// </summary>
188-
/// <param name="ex">The exception that was thrown and not handled in the instrumented method.</param>
189-
public void SetException(Exception ex)
190-
{
191-
_scope?.Span?.SetException(ex);
192-
}
193-
194-
/// <summary>
195-
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
196-
/// </summary>
197-
public void Dispose()
198-
{
199-
try
200-
{
201-
// sometimes, if an exception was unhandled in user code, status code is set to 500 later in the pipeline,
202-
// so it is still 200 here. if there was an unhandled exception, always set status code to 500.
203-
if (_scope?.Span?.Error == true)
204-
{
205-
_scope?.Span?.SetTag(Tags.HttpStatusCode, "500");
206-
}
207-
else if (_httpContext != null)
208-
{
209-
_scope?.Span?.SetTag(Tags.HttpStatusCode, _httpContext.Response.StatusCode.ToString());
210-
}
211-
}
212-
finally
213-
{
214-
_scope?.Dispose();
180+
scope?.Dispose();
215181
}
216182
}
217183
}

0 commit comments

Comments
 (0)