Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
5e25d91
initial
jcstorms1 Nov 12, 2025
1a133d3
remove unnecessary xml param
jcstorms1 Nov 12, 2025
8fbf66a
Add unit tests for extractors
jcstorms1 Nov 12, 2025
2e616ea
remove tracer paramater from integration tests
jcstorms1 Nov 12, 2025
fbaca0d
Fix unit tests
jcstorms1 Nov 12, 2025
1eb9c18
fix unit tests
jcstorms1 Nov 13, 2025
22c828b
add proxy when not aspnetcore
jcstorms1 Nov 24, 2025
f0e2ff5
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Nov 25, 2025
80fdf63
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Nov 26, 2025
548370a
working trace
jcstorms1 Dec 4, 2025
a9591b9
initial
jcstorms1 Nov 12, 2025
a400bd4
remove unnecessary xml param
jcstorms1 Nov 12, 2025
3d21dfe
Add unit tests for extractors
jcstorms1 Nov 12, 2025
32ed695
remove tracer paramater from integration tests
jcstorms1 Nov 12, 2025
301949c
Fix unit tests
jcstorms1 Nov 12, 2025
30152ca
fix unit tests
jcstorms1 Nov 13, 2025
8cdf247
add proxy when not aspnetcore
jcstorms1 Nov 24, 2025
b908490
working trace
jcstorms1 Dec 4, 2025
d55d815
Merge branch 'storms/add-azure-apim-proxy' of github.com:DataDog/dd-t…
jcstorms1 Dec 16, 2025
81aa696
removed all but 3 tags
jcstorms1 Jan 7, 2026
3285832
remove additional tags
jcstorms1 Jan 7, 2026
b97ec4c
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Jan 7, 2026
21f1370
minor clean up
jcstorms1 Jan 7, 2026
204f0fa
additional tests
jcstorms1 Jan 7, 2026
6a2291d
remove csproj changes
jcstorms1 Jan 7, 2026
a587b44
spacing
jcstorms1 Jan 7, 2026
52a2b63
comments and nits
jcstorms1 Jan 15, 2026
6013731
restore azure functions csproj
jcstorms1 Jan 16, 2026
a8aefa2
add space
jcstorms1 Jan 16, 2026
5a26d82
readd space
jcstorms1 Jan 16, 2026
7d2b388
remove unused proxyname var
jcstorms1 Jan 16, 2026
c2c21cf
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Jan 16, 2026
d657e14
add start time to proxy helper test
jcstorms1 Jan 16, 2026
7ae42ca
Merge branch 'storms/add-azure-apim-proxy' of github.com:DataDog/dd-t…
jcstorms1 Jan 16, 2026
36b2af9
remove unused import
jcstorms1 Jan 16, 2026
5a20d33
fix proxy span helper test
jcstorms1 Jan 16, 2026
c06d39f
fix azureproxyextractor unit tests
jcstorms1 Jan 16, 2026
f6aae2a
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Jan 21, 2026
a301ddf
remove redundant tests
jcstorms1 Jan 22, 2026
a795862
refactor and fix timestamp issues
jcstorms1 Jan 23, 2026
79d390a
fix timestamps in inferredproxyspanhelpertests
jcstorms1 Jan 23, 2026
3d41bb7
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Jan 26, 2026
8f54fb0
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Jan 28, 2026
9b372a6
fix lost worker span and minor changes
jcstorms1 Jan 30, 2026
15d517e
Address initial comments
jcstorms1 Feb 3, 2026
7540a26
add and update tests
jcstorms1 Feb 3, 2026
9870aec
remove grpc struct
jcstorms1 Feb 4, 2026
707e511
update snapshots for span.kind change on proxy spans
jcstorms1 Feb 4, 2026
f6a7a67
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Feb 4, 2026
3aae9b3
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Feb 10, 2026
31c9a13
add function proxy integration test
jcstorms1 Feb 10, 2026
31f2a05
remove unecessary code block and changes
jcstorms1 Feb 10, 2026
2f10a13
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Feb 17, 2026
6d000d2
remove more unnecessary code from worker process
jcstorms1 Feb 17, 2026
3715692
refactor and nullable references
jcstorms1 Feb 17, 2026
44cbe27
comments
jcstorms1 Feb 18, 2026
d87fd91
adding integration test and minor fixes
jcstorms1 Feb 23, 2026
c17569d
Merge branch 'master' into storms/add-azure-apim-proxy
jcstorms1 Feb 23, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Collections.Generic;
using Datadog.Trace.Activity.DuckTypes;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.ServiceBus;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared;
using Datadog.Trace.Configuration;
using Datadog.Trace.DataStreamsMonitoring;
using Datadog.Trace.DuckTyping;
Expand Down Expand Up @@ -77,7 +78,7 @@ public void ActivityStopped<T>(string sourceName, T activity)
payloadSize ?? 0,
0);

dataStreamsManager.InjectPathwayContextAsBase64String(span.Context.PathwayContext, new ServiceBusHeadersCollectionAdapter(applicationProperties));
dataStreamsManager.InjectPathwayContextAsBase64String(span.Context.PathwayContext, new AzureHeadersCollectionAdapter(applicationProperties));

// Close the scope and return so we bypass the common code path
span.Finish(activity.StartTimeUtc.Add(activity.Duration));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,10 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc

// AAS tags need to be set on any span for the backend to properly handle the billing.
// That said, it's more intuitive to find it on the local root for the customer.
if (model.TraceChunk.IsRunningInAzureAppService && model.TraceChunk.AzureAppServiceSettings is { } azureAppServiceSettings)
// Skip adding AAS tags to inferred proxy spans as they represent infrastructure outside the AAS environment
if (model.TraceChunk.IsRunningInAzureAppService &&
model.TraceChunk.AzureAppServiceSettings is { } azureAppServiceSettings &&
!(span.Tags is InferredProxyTags proxyTags && proxyTags.InferredSpan == 1.0))
{
// Done here to avoid initializing in most cases
InitializeAasTags();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;
using Datadog.Trace.ClrProfiler.CallTarget;
using Datadog.Trace.Configuration;
using Datadog.Trace.DuckTyping;
using Datadog.Trace.Headers;
using Datadog.Trace.Logging;
using Datadog.Trace.Propagators;
using Datadog.Trace.Tagging;
using Datadog.Trace.Util;
using Datadog.Trace.Vendors.Newtonsoft.Json;

#nullable enable
Expand All @@ -29,6 +32,7 @@ internal static class AzureFunctionsCommon

public const string OperationName = "azure_functions.invoke";
public const string SpanType = SpanTypes.Serverless;
public const string AzureApim = "azure.apim";
public const IntegrationId IntegrationId = Configuration.IntegrationId.AzureFunctions;

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

var functionName = instanceParam.FunctionDescriptor.ShortName;

// 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;
// Ignoring null because guaranteed running in AAS
if (tracer.Settings.AzureAppServiceMetadata is { IsIsolatedFunctionsApp: true }
&& tracer.InternalActiveScope is { } activeScope)
Expand All @@ -127,18 +132,21 @@ public static CallTargetState OnFunctionExecutionBegin<TTarget, TFunction>(TTarg
// and update it to be a "serverless" span.
var rootSpan = activeScope.Root.Span;

// The shortname is prefixed with "Functions.", so strip that off
var remoteFunctionName = functionName?.StartsWith("Functions.") == true
? functionName.Substring(10)
: functionName;

AzureFunctionsTags.SetRootSpanTags(
rootSpan,
shortName: remoteFunctionName,
fullName: rootSpan.Tags is AzureFunctionsTags t ? t.FullName : null, // can't get anything meaningful here, so leave it as-is
bindingSource: bindingSourceType.FullName,
triggerType: triggerType);
rootSpan.Type = SpanType;
if (!isProxySpan)
{
// The shortname is prefixed with "Functions.", so strip that off
var remoteFunctionName = functionName?.StartsWith("Functions.") == true
? functionName.Substring(10)
: functionName;
AzureFunctionsTags.SetRootSpanTags(
rootSpan,
shortName: remoteFunctionName,
fullName: rootSpan.Tags is AzureFunctionsTags t ? t.FullName : null, // can't get anything meaningful here, so leave it as-is
bindingSource: bindingSourceType.FullName,
triggerType: triggerType);
rootSpan.Type = SpanType;
}

return null;
}

Expand All @@ -159,15 +167,23 @@ public static CallTargetState OnFunctionExecutionBegin<TTarget, TFunction>(TTarg
else
{
scope = tracer.StartActiveInternal(OperationName);
AzureFunctionsTags.SetRootSpanTags(
scope.Root.Span,
shortName: functionName,
fullName: instanceParam.FunctionDescriptor.FullName,
bindingSource: bindingSourceType.FullName,
triggerType: triggerType);

if (!isProxySpan)
{
AzureFunctionsTags.SetRootSpanTags(
scope.Root.Span,
shortName: functionName,
fullName: instanceParam.FunctionDescriptor.FullName,
bindingSource: bindingSourceType.FullName,
triggerType: triggerType);
}
}

if (!isProxySpan)
{
scope.Root.Span.Type = SpanType;
}

scope.Root.Span.Type = SpanType;
scope.Span.ResourceName = $"{triggerType} {functionName}";
scope.Span.Type = SpanType;
tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId);
Expand Down Expand Up @@ -254,38 +270,40 @@ _ when type.StartsWith("eventGrid", StringComparison.OrdinalIgnoreCase) => "Even
}

var functionName = functionContext.FunctionDefinition.Name;

var tags = new AzureFunctionsTags
{
TriggerType = triggerType,
ShortName = functionName,
FullName = functionContext.FunctionDefinition.EntryPoint,
};

if (tracer.InternalActiveScope == null)
{
// This is the root scope
var tags = new AzureFunctionsTags
{
TriggerType = triggerType,
ShortName = functionName,
FullName = functionContext.FunctionDefinition.EntryPoint,
};
tags.SetAnalyticsSampleRate(IntegrationId, tracer.CurrentTraceSettings.Settings, enabledWithGlobalSetting: false);
scope = tracer.StartActiveInternal(OperationName, tags: tags, parent: extractedContext.SpanContext);
}
else
{
// shouldn't be hit, but better safe than sorry
scope = tracer.StartActiveInternal(OperationName);
var rootSpan = scope.Root.Span;
AzureFunctionsTags.SetRootSpanTags(
rootSpan,
shortName: functionName,
fullName: functionContext.FunctionDefinition.EntryPoint,
bindingSource: rootSpan.Tags is AzureFunctionsTags t ? t.BindingSource : null,
triggerType: triggerType);
{
var rootSpan = scope.Root.Span;
AzureFunctionsTags.SetRootSpanTags(
rootSpan,
shortName: functionName,
fullName: functionContext.FunctionDefinition.EntryPoint,
bindingSource: rootSpan.Tags is AzureFunctionsTags t ? t.BindingSource : null,
triggerType: triggerType);
}
}

// change root span's type to "serverless"
scope.Root.Span.Type = SpanType;
{
// change root span's type to "serverless"
scope.Root.Span.Type = SpanType;
scope.Span.ResourceName = $"{triggerType} {functionName}";
scope.Span.Type = SpanType;
}

scope.Span.ResourceName = $"{triggerType} {functionName}";
scope.Span.Type = SpanType;
tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId);
}
catch (Exception ex)
Expand Down Expand Up @@ -354,21 +372,7 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging<T>(T co
{
try
{
if (context.Features == null)
{
return default;
}

GrpcBindingsFeatureStruct? bindingsFeature = null;
foreach (var kvp in context.Features)
{
if (kvp.Key.FullName?.Equals("Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature") == true)
{
bindingsFeature = kvp.Value?.TryDuckCast<GrpcBindingsFeatureStruct>(out var feature) == true ? feature : null;
break;
}
}

var bindingsFeature = GetFeatureFromContext<T, FunctionBindingsFeatureStruct>(context, "Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature");
if (bindingsFeature == null)
{
return default;
Expand Down Expand Up @@ -459,6 +463,26 @@ private static bool AreAllSpanContextsIdentical(List<PropagationContext> context
ctx.SpanContext.TraceId128 == first!.TraceId128 &&
ctx.SpanContext.SpanId == first.SpanId);
}

private static TFeature? GetFeatureFromContext<T, TFeature>(T context, string featureTypeName)
where T : IFunctionContext
where TFeature : struct
{
if (context.Features == null)
{
return null;
}

foreach (var kvp in context.Features)
{
if (kvp.Key.FullName == featureTypeName)
{
return kvp.Value?.TryDuckCast<TFeature>(out var feature) == true ? feature : null;
}
}

return null;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Functions;
[DuckCopy]
internal struct FunctionBindingsFeatureStruct
{
public IDictionary<string, object?>? TriggerMetadata;
public IDictionary<string, object?>? InputData;
}

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.ComponentModel;
using System.Threading;
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared;
using Datadog.Trace.ClrProfiler.CallTarget;
using Datadog.Trace.Configuration;
using Datadog.Trace.DataStreamsMonitoring;
Expand Down Expand Up @@ -61,7 +62,7 @@ internal static CallTargetState OnMethodBegin<TTarget, TMessage>(TTarget instanc

if (message.ApplicationProperties is not null)
{
var headers = new ServiceBusHeadersCollectionAdapter(message.ApplicationProperties);
var headers = new AzureHeadersCollectionAdapter(message.ApplicationProperties);

try
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="ServiceBusHeadersCollectionAdapter.cs" company="Datadog">
// <copyright file="AzureHeadersCollectionAdapter.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>
Expand All @@ -7,17 +7,15 @@

using System;
using System.Collections.Generic;
using System.Text;
using Datadog.Trace.Headers;
using Datadog.Trace.Logging;

namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.ServiceBus;
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared;

internal readonly struct ServiceBusHeadersCollectionAdapter : IHeadersCollection
internal readonly struct AzureHeadersCollectionAdapter : IHeadersCollection
{
private readonly IDictionary<string, object> _properties;

public ServiceBusHeadersCollectionAdapter(IDictionary<string, object> properties)
public AzureHeadersCollectionAdapter(IDictionary<string, object> properties)
{
_properties = properties;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,19 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;
internal sealed class AwsApiGatewayExtractor : IInferredProxyExtractor
{
// This is the expected value of the x-dd-proxy header
private const string ProxyName = "aws-apigateway";

private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor<AwsApiGatewayExtractor>();

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

try
{
if (tracer?.Settings.InferredProxySpansEnabled != true)
{
return false;
}

// we need to first validate whether or not we have the header and whether or not it matches aws-apigateway
var proxyName = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.Name);
if (proxyName is null || !string.Equals(proxyName, ProxyName, StringComparison.OrdinalIgnoreCase))
{
Log.Debug("Invalid or missing {HeaderName} header with value {Value}", InferredProxyHeaders.Name, proxyName);
return false;
}

var startTimeHeaderValue = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.StartTime);

// we also need to validate that we have the start time header otherwise we won't be able to create the span
if (!GetStartTime(startTimeHeaderValue, out var startTime))
if (!InferredProxySpanHelper.GetStartTime(startTimeHeaderValue, out var startTime))
{
return false;
}
Expand All @@ -56,7 +41,7 @@ public bool TryExtract<TCarrier, TCarrierGetter>(TCarrier carrier, TCarrierGette
var path = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.Path);
var stage = ParseUtility.ParseString(carrier, carrierGetter, InferredProxyHeaders.Stage);

data = new InferredProxyData(proxyName, startTime, domainName, httpMethod, path, stage);
data = new InferredProxyData(InferredProxySpanHelper.AwsProxyHeaderValue, startTime, domainName, httpMethod, path, stage, null);

if (Log.IsEnabled(LogEventLevel.Debug))
{
Expand All @@ -69,35 +54,7 @@ public bool TryExtract<TCarrier, TCarrierGetter>(TCarrier carrier, TCarrierGette
}
catch (Exception ex)
{
Log.Error(ex, "Error extracting proxy data from {Proxy} headers", ProxyName);
return false;
}
}

private static bool GetStartTime(string? startTime, out DateTimeOffset start)
{
start = default;

if (string.IsNullOrEmpty(startTime))
{
Log.Debug("Missing header '{HeaderName}'", InferredProxyHeaders.StartTime);
return false;
}

if (!long.TryParse(startTime, out var startTimeMs))
{
Log.Warning("Failed to parse header '{HeaderName}' with value '{Value}'", InferredProxyHeaders.StartTime, startTime);
return false;
}

try
{
start = DateTimeOffset.FromUnixTimeMilliseconds(startTimeMs);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to convert value '{Value}' from header '{HeaderName}' to DateTimeOffset", InferredProxyHeaders.StartTime, startTimeMs);
Log.Error(ex, "Error extracting proxy data from {Proxy} headers", InferredProxySpanHelper.AwsProxyHeaderValue);
return false;
}
}
Expand Down
Loading
Loading