Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ internal static class AzureFunctionsCommon
public const string IntegrationName = nameof(Configuration.IntegrationId.AzureFunctions);
public const string OperationName = AzureFunctionsConstants.AzureFunctionName;
public const string AzureApim = AzureFunctionsConstants.AzureApimName;
public const string AzureFrontDoor = AzureFunctionsConstants.AzureFrontDoorName;
public const IntegrationId IntegrationId = Configuration.IntegrationId.AzureFunctions;

private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(AzureFunctionsCommon));
Expand Down Expand Up @@ -129,8 +130,9 @@ public static CallTargetState OnFunctionExecutionBegin<TTarget, TFunction>(TTarg
}

var functionName = instanceParam.FunctionDescriptor.ShortName;
var opName = tracer.InternalActiveScope?.Root.Span.OperationName;
// Check if there's an inferred proxy span (e.g., azure.apim) that we shouldn't overwrite
var isProxySpan = tracer.InternalActiveScope?.Root.Span.OperationName == AzureApim;
var isProxySpan = opName == AzureApim || opName == AzureFrontDoor;
Comment thread
TophrC-dd marked this conversation as resolved.
// Ignoring null because guaranteed running in AAS
if (tracer.Settings.AzureAppServiceMetadata is { IsIsolatedFunctionsApp: true }
&& tracer.InternalActiveScope is { } activeScope)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ internal static class AzureFunctionsConstants
// Used for the operation name of spans created for Azure API Management requests
public const string AzureApimName = "azure.apim";

// Used for the operation name of the spans created for Azure Front Door requests
public const string AzureFrontDoorName = "azure.frontdoor";

// Used for the operation name of spans created for Azure Functions requests
public const string AzureFunctionName = "azure_functions.invoke";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// <copyright file="AzureFrontDoorExtractor.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#nullable enable

using System;
using Datadog.Trace.Logging;
using Datadog.Trace.Propagators;
using Datadog.Trace.Vendors.Serilog.Events;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;

/// <summary>
/// Extracts proxy metadata from Azure API Management headers.
/// </summary>
internal sealed class AzureFrontDoorExtractor : IInferredProxyExtractor
{
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<AzureFrontDoorExtractor>();

public bool TryExtract<TCarrier, TCarrierGetter>(TCarrier carrier, TCarrierGetter carrierGetter, out InferredProxyData data)
where TCarrierGetter : struct, ICarrierGetter<TCarrier>
{
data = default;

try
{

var startTimeHeaderValue = InferredProxyHeaders.StartTime ?
Comment thread
TophrC-dd marked this conversation as resolved.
Outdated
ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.StartTime ) : DateTimeOffset.FromUnixTimeMilliseconds().ToString();

// we also need to validate that we have the start time header otherwise we won't be able to create the span
if (!InferredProxySpanHelper.GetStartTime(startTimeHeaderValue, out var startTime))
{
return false;
}

// the remaining headers aren't necessarily required
var domainName = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.Domain);
var httpMethod = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.HttpMethod);
var path = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.Path);
var region = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.Region);
data = new InferredProxyData(InferredProxySpanHelper.AzureProxyHeaderValue, startTime, domainName, httpMethod, path, null, region);

if (Log.IsEnabled(LogEventLevel.Debug))
{
Log.Debug(
"Successfully extracted proxy data: StartTime={StartTime}, Domain={Domain}, Method={Method}, Path={Path}, Region={Region}",
[startTimeHeaderValue, domainName, httpMethod, path, region]);
}

return true;
}
catch (Exception ex)
{
Log.Error(ex, "Error extracting proxy data from {Proxy} headers", InferredProxySpanHelper.AzureProxyHeaderValue);
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// <copyright file="AzureFrontDoorSpanFactory.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#nullable enable

using System;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared;
using Datadog.Trace.Logging;
using Datadog.Trace.Tagging;
using Datadog.Trace.Util;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;

/// <summary>
/// Creates spans representing requests handled by Azure API Management.
/// </summary>
internal sealed class AzureFrontDoorSpanFactory : IInferredSpanFactory
{
private const string OperationName = AzureFunctionsConstants.AzureFrontDoorName;
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<AzureFrontDoorSpanFactory>();

public Scope? CreateSpan(Tracer tracer, InferredProxyData data, ISpanContext? parent = null)
{
try
{
var resourceUrl = data.Path is null ? string.Empty : UriHelpers.GetCleanUriPath(data.Path).ToLowerInvariant();

var tags = new InferredProxyTags
{
HttpMethod = data.HttpMethod,
InstrumentationName = data.ProxyName,
HttpUrl = $"{data.DomainName}{data.Path}",
HttpRoute = resourceUrl,
InferredSpan = 1,
Region = data.Region,
};

var scope = tracer.StartActiveInternal(operationName: OperationName, parent: parent, startTime: data.StartTime, tags: tags, serviceName: data.DomainName, serviceNameSource: "azure-frontdoor");
scope.Span.ResourceName = data.HttpMethod is null ? resourceUrl : $"{data.HttpMethod} {resourceUrl}";
scope.Span.Type = SpanTypes.Web;

return scope;
}
catch (Exception ex)
{
Log.Error(ex, "Error creating Azure API Management span");
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;
internal static class InferredProxySpanHelper
{
public const string AzureProxyHeaderValue = "azure-apim";
public const string AzureFrontDoorHeaderValue = "azure-fd";
public const string AwsProxyHeaderValue = "aws-apigateway";
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(InferredProxySpanHelper));
private static InferredProxyCoordinator? _awsCoordinator;
Expand Down Expand Up @@ -58,6 +59,12 @@ internal static class InferredProxySpanHelper
return _azureCoordinator.ExtractAndCreateScope(tracer, carrier, accessor, propagationContext);
}

if (string.Equals(proxyName, AzureFrontDoorHeaderValue, StringComparison.OrdinalIgnoreCase))
{
_azureCoordinator ??= new InferredProxyCoordinator(new AzureFrontDoorExtractor(), new Az());
Comment thread
TophrC-dd marked this conversation as resolved.
Outdated
return _azureCoordinator.ExtractAndCreateScope(tracer, carrier, accessor, propagationContext);
}

if (string.Equals(proxyName, AwsProxyHeaderValue, StringComparison.OrdinalIgnoreCase))
{
_awsCoordinator ??= new InferredProxyCoordinator(new AwsApiGatewayExtractor(), new AwsApiGatewaySpanFactory());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// <copyright file="AzureFrontDoorExtractorTests.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

using System;
using System.Globalization;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;
using Datadog.Trace.Headers;
using FluentAssertions;
using Xunit;

namespace Datadog.Trace.ClrProfiler.Managed.Tests.AutoInstrumentation.Proxy;

public class AzureFrontDoorExtractorTests
{
private readonly AzureApiManagementExtractor _extractor;

public AzureApiManagementExtractorTests()
{
_extractor = new AzureApiManagementExtractor();
}

[Fact]
public void TryExtract_WithAllValidHeaders_ReturnsTrue()
{
// this reduces precision to 1ms, so we can't compare extracted value to the original DateTimeOffset directly
var unixTimeMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var start = DateTimeOffset.FromUnixTimeMilliseconds(unixTimeMilliseconds);

var headers = ProxyTestHelpers.CreateValidAzureFrontDoorHeaders(unixTimeMilliseconds.ToString());

var success = _extractor.TryExtract(headers, headers.GetAccessor(), out var data);

success.Should().BeTrue();
data.ProxyName.Should().Be("azure-frontdoor");
data.StartTime.Should().Be(start);
data.DomainName.Should().BeNull(); // Azure doesn't use domain
data.HttpMethod.Should().Be("POST");
data.Path.Should().Be("/api/v1/users");
data.Stage.Should().Be("prod");
data.Region.Should().Be("canada central");
}

[Fact]
public void TryExtract_WithMinimumValidHeaders_ReturnsTrue()
{
// this reduces precision to 1ms, so we can't compare extracted value to the original DateTimeOffset directly
var unixTimeMilliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var start = DateTimeOffset.FromUnixTimeMilliseconds(unixTimeMilliseconds);

var headers = ProxyTestHelpers.CreateValidAzureFrontDoorHeaders(unixTimeMilliseconds.ToString());
headers.Remove(InferredProxyHeaders.HttpMethod);
headers.Remove(InferredProxyHeaders.Path);
headers.Remove(InferredProxyHeaders.Region);

var success = _extractor.TryExtract(headers, headers.GetAccessor(), out var data);

success.Should().BeTrue();
data.ProxyName.Should().Be("azure-frontdoor");
data.StartTime.Should().Be(start);
data.DomainName.Should().BeNull();
data.HttpMethod.Should().BeNull();
data.Path.Should().BeNull();
data.Stage.Should().BeNull();
data.Region.Should().BeNull();
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("invalid")]
[InlineData("1111111122222222333333334444444455555555666666667777777788888888")] // too large
public void TryExtract_WithInvalidStartTime_ReturnsFalse(string startTime)
{
var headers = ProxyTestHelpers.CreateValidAzureFrontDoorHeaders();
headers.Set(InferredProxyHeaders.StartTime, startTime);

var success = _extractor.TryExtract(headers, headers.GetAccessor(), out var data);

success.Should().BeFalse();
data.Should().Be(default(InferredProxyData));
}

[Fact]
public void TryExtract_WithMissingStartTime_ReturnsFalse()
{
var headers = ProxyTestHelpers.CreateValidAzureHeaders();
headers.Remove(InferredProxyHeaders.StartTime);

var success = _extractor.TryExtract(headers, headers.GetAccessor(), out var data);

success.Should().BeFalse();
data.Should().Be(default(InferredProxyData));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// <copyright file="AzureFrontDoorSpanFactoryTests.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#nullable enable

using System;
using System.Threading.Tasks;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;
using Datadog.Trace.TestHelpers.TestTracer;
using FluentAssertions;
using Xunit;

namespace Datadog.Trace.ClrProfiler.Managed.Tests.AutoInstrumentation.Proxy;

public class AzureFrontDoorSpanFactoryTests
{
[Fact]
public async Task CreateSpan_CreatesSpanWithCorrectProperties()
{
var factory = new AzureFrontDoorSpanFactory();
await using var tracer = ProxyTestHelpers.GetMockTracer();
var startTime = DateTimeOffset.UtcNow;
var data = new InferredProxyData("azure-frontdoor", startTime, "myapp.azurefd.net", "GET", "/api/v1/users", null, null);

var scope = factory.CreateSpan(tracer, data);

scope.Should().NotBeNull();
var span = scope!.Span;
span.Should().NotBeNull();
span.OperationName.Should().Be("azure.frontdoor");
span.ResourceName.Should().Be("GET /api/v1/users"); // TODO obfuscation and quantization
span.Type.Should().Be("web");
span.StartTime.Should().Be(startTime);

var tags = scope.Span.Tags;
tags.Should().NotBeNull();
tags.GetTag(Tags.HttpMethod).Should().Be("GET");
tags.GetTag(Tags.InstrumentationName).Should().Be("azure-frontdoor");
tags.GetTag(Tags.HttpUrl).Should().Be("myapp.azurefd.net/api/v1/users");
tags.GetTag(Tags.HttpRoute).Should().Be("/api/v1/users");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,19 @@ internal static NameValueHeadersCollection CreateValidAzureHeaders(string? start
headers.Set(InferredProxyHeaders.Region, "canada central");
return headers;
}

internal static NameValueHeadersCollection CreateValidAzureFrontDoorHeaders(string? start = null)
Comment thread
TophrC-dd marked this conversation as resolved.
{
start ??= DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture);

var headers = new NameValueHeadersCollection([]);
headers.Set(InferredProxyHeaders.Name, "azure-frontdoor");
headers.Set(InferredProxyHeaders.StartTime, start);
headers.Set(InferredProxyHeaders.Domain, "myapp.azurefd.net");
headers.Set(InferredProxyHeaders.HttpMethod, "GET");
headers.Set(InferredProxyHeaders.Path, "/api/test");
headers.Set(InferredProxyHeaders.Stage, "prod");
headers.Set(InferredProxyHeaders.Region, "canada central");
return headers;
}
}
Loading