Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get transaction name from Web API controller route template #1189

Merged
merged 2 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .ci/windows/msbuild-tools.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
#

# Install visualstudio2019buildtools
choco install visualstudio2019buildtools -m -y --no-progress --force -r --version=16.8.5.0
choco install visualstudio2019buildtools -m -y --no-progress --force -r --version=16.9.0.0
if ($LASTEXITCODE -ne 0) {
Write-Host "visualstudio2019buildtools installation failed."
exit 1
}
choco install visualstudio2019professional -m -y --no-progress --force -r --version=16.8.5.0 --package-parameters "--includeRecommended --includeOptional"
choco install visualstudio2019professional -m -y --no-progress --force -r --version=16.9.0.0 --package-parameters "--includeRecommended --includeOptional"
if ($LASTEXITCODE -ne 0) {
Write-Host "visualstudio2019professional installation failed."
exit 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
<Compile Include="Mvc\JsonBadRequestResult.cs" />
<Compile Include="Mvc\JsonNetValueProviderFactory.cs" />
<Compile Include="Mvc\StreamResult.cs" />
<Compile Include="Controllers\AttributeRoutingWebApiController.cs" />
</ItemGroup>
<ItemGroup Condition="'$(OS)' == 'WINDOWS_NT'">
<Content Include="Content\bootstrap-grid.css" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Licensed to Elasticsearch B.V under
// one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Web.Http;

namespace AspNetFullFrameworkSampleApp.Controllers
{
[RoutePrefix(RoutePrefix)]
public class AttributeRoutingWebApiController : ApiController
{
public const string RoutePrefix = "api/AttributeRoutingWebApi";
public const string Route = "{id}";

[HttpGet]
[Route(Route)]
public IHttpActionResult Get(string id) =>
Ok($"attributed routed web api controller {id}");
}
}
64 changes: 62 additions & 2 deletions src/Elastic.Apm.AspNetFullFramework/ElasticApmModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
Expand All @@ -15,6 +16,7 @@
using Elastic.Apm.Helpers;
using Elastic.Apm.Logging;
using Elastic.Apm.Model;
using Elastic.Apm.Reflection;

namespace Elastic.Apm.AspNetFullFramework
{
Expand All @@ -30,6 +32,8 @@ public class ElasticApmModule : IHttpModule
private readonly string _dbgInstanceName;
private HttpApplication _application;
private IApmLogger _logger;
private Type _httpRouteDataInterfaceType;
private Func<object, string> _routeDataTemplateGetter;

public ElasticApmModule() =>
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
Expand Down Expand Up @@ -77,6 +81,7 @@ private void InitImpl(HttpApplication application)
PlatformDetection.DotNetRuntimeDescription, HttpRuntime.IISVersion);
}

_routeDataTemplateGetter = CreateWebApiAttributeRouteTemplateGetter();
_application = application;
_application.BeginRequest += OnBeginRequest;
_application.EndRequest += OnEndRequest;
Expand Down Expand Up @@ -285,8 +290,31 @@ private void ProcessEndRequest(object sender)
else
routeData = values;

_logger?.Trace()?.Log("Calculating transaction name based on route data");
var name = Transaction.GetNameFromRouteContext(routeData);
string name = null;

// if we're dealing with Web API attribute routing, get transaction name from the route template
if (routeData.TryGetValue("MS_SubRoutes", out var template) && _httpRouteDataInterfaceType != null)
{
if (template is IEnumerable enumerable)
{
var enumerator = enumerable.GetEnumerator();
if (enumerator.MoveNext())
{
var subRoute = enumerator.Current;
if (subRoute != null && _httpRouteDataInterfaceType.IsInstanceOfType(subRoute))
{
_logger?.Trace()?.Log("Calculating transaction name from web api attribute routing");
name = _routeDataTemplateGetter(subRoute);
}
}
}
}
else
{
_logger?.Trace()?.Log("Calculating transaction name based on route data");
name = Transaction.GetNameFromRouteContext(routeData);
}

if (!string.IsNullOrWhiteSpace(name)) transaction.Name = $"{context.Request.HttpMethod} {name}";
}
else
Expand Down Expand Up @@ -427,5 +455,37 @@ private static void SafeAgentSetup(string dbgInstanceName)
agentComponents.Dispose();
}
}

/// <summary>
/// Compiles a delegate from a lambda expression to get the route template from HttpRouteData when
/// System.Web.Http is referenced.
/// </summary>
private Func<object, string> CreateWebApiAttributeRouteTemplateGetter()
{
_httpRouteDataInterfaceType = Type.GetType("System.Web.Http.Routing.IHttpRouteData,System.Web.Http");
if (_httpRouteDataInterfaceType != null)
{
var routePropertyInfo = _httpRouteDataInterfaceType.GetProperty("Route");
if (routePropertyInfo != null)
{
var routeType = routePropertyInfo.PropertyType;
var routeTemplatePropertyInfo = routeType.GetProperty("RouteTemplate");
if (routeTemplatePropertyInfo != null)
{
var routePropertyGetter = ExpressionBuilder.BuildPropertyGetter(_httpRouteDataInterfaceType, routePropertyInfo);
var routeTemplatePropertyGetter = ExpressionBuilder.BuildPropertyGetter(routeType, routeTemplatePropertyInfo);
return routeData =>
{
var route = routePropertyGetter(routeData);
return route is null
? null
: routeTemplatePropertyGetter(route) as string;
};
}
}
}

return null;
}
}
}
38 changes: 38 additions & 0 deletions src/Elastic.Apm/Reflection/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to Elasticsearch B.V under
// one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System;
using System.Linq.Expressions;
using System.Reflection;

namespace Elastic.Apm.Reflection
{
internal class ExpressionBuilder
{
/// <summary>
/// Builds a delegate to get a property of type <typeparamref name="TProperty"/> from an object
/// of type <typeparamref name="TObject"/>
/// </summary>
public static Func<TObject, TProperty> BuildPropertyGetter<TObject, TProperty>(string propertyName)
{
var parameterExpression = Expression.Parameter(typeof(TObject), "value");
var memberExpression = Expression.Property(parameterExpression, propertyName);
return Expression.Lambda<Func<TObject, TProperty>>(memberExpression, parameterExpression).Compile();
}

/// <summary>
/// Builds a delegate to get a property from an object. <paramref name="type"/> is cast to <see cref="Object"/>,
/// with the returned property cast to <see cref="Object"/>.
/// </summary>
public static Func<object, object> BuildPropertyGetter(Type type, PropertyInfo propertyInfo)
{
var parameterExpression = Expression.Parameter(typeof(object), "value");
var parameterCastExpression = Expression.Convert(parameterExpression, type);
var memberExpression = Expression.Property(parameterCastExpression, propertyInfo);
var returnCastExpression = Expression.Convert(memberExpression, typeof(object));
return Expression.Lambda<Func<object, object>>(returnCastExpression, parameterExpression).Compile();
}
}
}
3 changes: 3 additions & 0 deletions test/Elastic.Apm.AspNetFullFramework.Tests/TestsBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ internal static class SampleAppUrlPaths
internal static readonly SampleAppUrlPathData WebApiPage =
new SampleAppUrlPathData(WebApiController.Path, 200);

internal static SampleAppUrlPathData AttributeRoutingWebApiPage(string id) =>
new SampleAppUrlPathData(AttributeRoutingWebApiController.RoutePrefix + "/" + id, 200);

internal static readonly SampleAppUrlPathData WebformsPage =
new SampleAppUrlPathData(nameof(Webforms) + ".aspx", 200);

Expand Down
15 changes: 15 additions & 0 deletions test/Elastic.Apm.AspNetFullFramework.Tests/TransactionNameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using AspNetFullFrameworkSampleApp.Controllers;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
Expand Down Expand Up @@ -118,6 +119,20 @@ await WaitAndCustomVerifyReceivedData(receivedData =>
});
}

[AspNetFullFrameworkFact]
public async Task Name_Should_Be_RouteTemplate_When_WebApi_Attribute_Routing()
{
var pathData = SampleAppUrlPaths.AttributeRoutingWebApiPage("foo");
await SendGetRequestToSampleAppAndVerifyResponse(pathData.Uri, pathData.StatusCode);

await WaitAndCustomVerifyReceivedData(receivedData =>
{
receivedData.Transactions.Count.Should().Be(1);
var transaction = receivedData.Transactions.Single();
transaction.Name.Should().Be($"GET {AttributeRoutingWebApiController.RoutePrefix}/{AttributeRoutingWebApiController.Route}");
});
}

[AspNetFullFrameworkFact]
public async Task Name_Should_Be_Path_When_Webforms_Page()
{
Expand Down