From 27f01256474b0daf714073789880b3695a742fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Marti=CC=81nez=20Bernardo?= Date: Tue, 21 Jul 2026 08:53:54 +0200 Subject: [PATCH] [Azure Event Grid] Add Azure Functions support --- .azure-pipelines/ultimate-pipeline.yml | 4 + docker-compose.yml | 9 + docker/eventgrid-emulator-config.json | 3 +- .../Azure/Functions/AzureFunctionsCommon.cs | 74 ++----- .../Functions/EventGridFunctionsCommon.cs | 193 ++++++++++++++++++ .../Azure/Functions/FunctionBindingsCommon.cs | 66 ++++++ .../Configuration/TracerSettings.cs | 10 + .../supported-configurations.yaml | 10 + .../ConfigurationKeys.g.cs | 7 + .../ConfigurationKeys.g.cs | 7 + .../ConfigurationKeys.g.cs | 7 + .../ConfigurationKeys.g.cs | 7 + .../AzureFunctionsMessagingTriggerTests.cs | 65 +++++- .../Functions/AzureFunctionsCommonTests.cs | 4 +- .../EventGridFunctionsCommonTests.cs | 114 +++++++++++ .../Configuration/ConfigurationSourceTests.cs | 3 + ...ridOutputBinding_SubmitsTrace.verified.txt | 187 +++++++++++++++++ ...EventGridTrigger_SubmitsTrace.verified.txt | 187 +++++++++++++++++ .../MessagingTriggers.cs | 53 +++++ ...AzureFunctions.V4Isolated.Messaging.csproj | 2 + .../local.settings.json | 2 + 21 files changed, 947 insertions(+), 67 deletions(-) create mode 100644 tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommon.cs create mode 100644 tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/FunctionBindingsCommon.cs create mode 100644 tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommonTests.cs create mode 100644 tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridOutputBinding_SubmitsTrace.verified.txt create mode 100644 tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridTrigger_SubmitsTrace.verified.txt diff --git a/.azure-pipelines/ultimate-pipeline.yml b/.azure-pipelines/ultimate-pipeline.yml index a3c572bf0259..4d47617e62c6 100644 --- a/.azure-pipelines/ultimate-pipeline.yml +++ b/.azure-pipelines/ultimate-pipeline.yml @@ -2412,6 +2412,9 @@ stages: SampleName: $(IntegrationTestSampleName) DD_LOGGER_DD_API_KEY: $(ddApiKey) COMPOSE_PROFILES: group$(dockerGroup) + # Event Grid emulator (group2) pushes to the Functions host webhook; in CI the func host runs inside + # the IntegrationTests container, reachable by sibling containers as "integrationtests". + EVENTGRID_WEBHOOK_HOST: integrationtests displayName: docker-compose build IntegrationTests and run StartDependencies (Group $(dockerGroup)) retryCountOnTaskFailure: 5 @@ -2431,6 +2434,7 @@ stages: DD_LOGGER_DD_API_KEY: $(ddApiKey) baseImage: $(baseImage) # for interpolation in the docker-compose file COMPOSE_PROFILES: group$(dockerGroup) + EVENTGRID_WEBHOOK_HOST: integrationtests - script: docker-compose -f docker-compose.yml -p $(DockerComposeProjectName)-g$(dockerGroup) logs displayName: docker-compose logs diff --git a/docker-compose.yml b/docker-compose.yml index d3e01e0a8f04..dcb7e6945a3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -280,6 +280,14 @@ services: - "127.0.0.1:6500:6500" volumes: - ./docker/eventgrid-emulator-config.json:/app/appsettings.json:ro + environment: + # Override the topic's subscriber webhook (ASP.NET array config). The Azure Functions host that + # receives the pushed event is reachable at a different host depending on where it runs: inside the + # IntegrationTests container in CI (EVENTGRID_WEBHOOK_HOST=integrationtests) vs on the Docker host + # locally (host.docker.internal). The emulator adds the aeg-event-type header and forwards the CloudEvent. + - Topics__samples-azure-functions-eventgrid-topic__0=http://${EVENTGRID_WEBHOOK_HOST:-host.docker.internal}:7071/runtime/webhooks/eventgrid?functionName=EventGridTrigger + extra_hosts: + - "host.docker.internal:host-gateway" cosmosdb-emulator: image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview@sha256:54d7bc334494c50cea867c270880671a7db080626a9732832b34c0d69342f9b0 @@ -465,6 +473,7 @@ services: - ASB_CONNECTION_STRING=Endpoint=sb://azureservicebus-emulator:5672;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true; - EVENTHUBS_CONNECTION_STRING=Endpoint=sb://azure-eventhubs-emulator:5672;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true; - EVENTGRID_TOPIC_ENDPOINT=http://azure-eventgrid-emulator:6500/samples-eventgrid-topic/api/events + - EVENTGRID_AZURE_FUNCTIONS_TOPIC_ENDPOINT=http://azure-eventgrid-emulator:6500/samples-azure-functions-eventgrid-topic/api/events - COSMOSDB_ENDPOINT=https://cosmosdb-emulator:8081 - TEST_AGENT_HOST=test-agent - CONTAINER_HOSTNAME=http://integrationtests diff --git a/docker/eventgrid-emulator-config.json b/docker/eventgrid-emulator-config.json index 757f3ef14cbe..e48b4d3f494d 100644 --- a/docker/eventgrid-emulator-config.json +++ b/docker/eventgrid-emulator-config.json @@ -1,5 +1,6 @@ { "Topics": { - "samples-eventgrid-topic": [] + "samples-eventgrid-topic": [], + "samples-azure-functions-eventgrid-topic": [] } } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommon.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommon.cs index 7b6ea4280fe0..e99df6bd4d7d 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommon.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommon.cs @@ -8,7 +8,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared; using Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy; @@ -20,9 +19,6 @@ using Datadog.Trace.PlatformHelpers; using Datadog.Trace.Propagators; using Datadog.Trace.Tagging; -using Datadog.Trace.Util; -using Datadog.Trace.Util.Json; -using Datadog.Trace.Vendors.Newtonsoft.Json; using Datadog.Trace.Vendors.Serilog.Events; #nullable enable @@ -314,6 +310,12 @@ _ when type.StartsWith("eventGrid", StringComparison.OrdinalIgnoreCase) => "Even case "EventHub" when tracer.CurrentTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AzureEventHubs): extractedContext = ExtractPropagatedContextFromMessaging(functionContext, "Properties", "PropertiesArray").MergeBaggageInto(Baggage.Current); break; + + case "EventGrid" when tracer.CurrentTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AzureEventGrid, defaultValue: false): + { + extractedContext = EventGridFunctionsCommon.CreateReceiveSpanContext(tracer, functionContext, entry.Key as string, Baggage.Current); + break; + } } break; @@ -483,23 +485,14 @@ private static PropagationContext ExtractPropagatedContextFromHttp(T function try { - object? feature = null; - foreach (var keyValuePair in functionContext.Features) - { - if (keyValuePair.Key.FullName?.Equals("Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature") == true) - { - feature = keyValuePair.Value; - break; - } - } - - if (feature is null || !feature.TryDuckCast(out var bindingFeature)) + var bindingsFeature = FunctionBindingsCommon.GetBindingsFeature(functionContext); + if (bindingsFeature is null) { return default; } - if (bindingFeature.InputData is null - || !bindingFeature.InputData.TryGetValue(bindingName!, out var requestDataObject) + if (bindingsFeature.Value.InputData is null + || !bindingsFeature.Value.InputData.TryGetValue(bindingName!, out var requestDataObject) || requestDataObject is null) { return default; @@ -524,7 +517,7 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging(T co { try { - var bindingsFeature = GetFeatureFromContext(context, "Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature"); + var bindingsFeature = FunctionBindingsCommon.GetBindingsFeature(context); if (bindingsFeature == null) { return default; @@ -535,7 +528,7 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging(T co // Extract from single message properties if (triggerMetadata?.TryGetValue(singlePropertyKey, out var singlePropsObj) == true && - TryParseJson>(singlePropsObj, out var singleProps) && singleProps != null) + FunctionBindingsCommon.TryParseJson>(singlePropsObj, out var singleProps)) { var singleContext = Shared.AzureMessagingCommon.ExtractContext(singleProps); if (singleContext.SpanContext != null) @@ -546,7 +539,7 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging(T co // Extract from batch properties array if (triggerMetadata?.TryGetValue(batchPropertyKey, out var arrayPropsObj) == true && - TryParseJson[]>(arrayPropsObj, out var propsArray) && propsArray != null) + FunctionBindingsCommon.TryParseJson[]>(arrayPropsObj, out var propsArray)) { foreach (var props in propsArray) { @@ -580,27 +573,6 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging(T co } } - private static bool TryParseJson(object? jsonObj, [NotNullWhen(true)] out T? result) - where T : class - { - result = null; - if (jsonObj is not string jsonString) - { - return false; - } - - try - { - result = JsonHelper.DeserializeObject(jsonString); - return result != null; - } - catch (Exception ex) - { - Log.Debug(ex, "Failed to parse JSON: {Json}", jsonString); - return false; - } - } - // Checks if all SpanContexts are identical (ignores baggage) private static bool AreAllSpanContextsIdentical(List contexts) { @@ -615,26 +587,6 @@ private static bool AreAllSpanContextsIdentical(List context ctx.SpanContext.TraceId128 == first!.TraceId128 && ctx.SpanContext.SpanId == first.SpanId); } - - private static TFeature? GetFeatureFromContext(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(out var feature) == true ? feature : null; - } - } - - return null; - } } } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommon.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommon.cs new file mode 100644 index 000000000000..c9438be47342 --- /dev/null +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommon.cs @@ -0,0 +1,193 @@ +// +// 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. +// + +#if !NETFRAMEWORK + +using System; +using System.Collections.Generic; +using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared; +using Datadog.Trace.Configuration; +using Datadog.Trace.Configuration.Schema; +using Datadog.Trace.Logging; +using Datadog.Trace.Propagators; +using Datadog.Trace.Tagging; +using Datadog.Trace.Util; + +#nullable enable + +namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Functions +{ + internal static class EventGridFunctionsCommon + { + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(EventGridFunctionsCommon)); + + /// + /// Creates an azure_eventgrid.receive consumer span that links to the producers' + /// azure_eventgrid.send spans (when W3C contexts were extracted from the CloudEvents), + /// and returns a pointing at the receive span so the + /// function-invoke span is parented under it. This mirrors the Event Hubs / Service Bus + /// receive-span topology (new trace, span-linked to the producer). + /// + internal static PropagationContext CreateReceiveSpanContext(Tracer tracer, T context, string? bindingName, Baggage destinationBaggage) + where T : IFunctionContext + { + var cloudEvents = GetCloudEvents(context, bindingName); + var producerContexts = ExtractPropagatedContexts(cloudEvents); + + // As with Service Bus and Event Hubs batches, use the first extracted producer + // context as the source of ambient baggage. + if (producerContexts.Count > 0) + { + producerContexts[0].MergeBaggageInto(destinationBaggage); + } + + return CreateReceiveSpan(tracer, cloudEvents, producerContexts); + } + + internal static List ExtractPropagatedContexts(Dictionary[] cloudEvents) + { + var extractedContexts = new List(); + + try + { + if (cloudEvents.Length == 0) + { + return extractedContexts; + } + + var uniqueSpanContexts = new HashSet(new SpanContextComparer()); + + foreach (var cloudEventProps in cloudEvents) + { + // Extract W3C trace context and baggage from CloudEvent extension attributes. + // These were injected by EventGridCommon.InjectW3CContext() on the publisher side. + var extractedContext = AzureMessagingCommon.ExtractContext(cloudEventProps); + if (extractedContext.SpanContext is { } spanContext && uniqueSpanContexts.Add(spanContext)) + { + extractedContexts.Add(extractedContext); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Error extracting propagated context from EventGrid binding"); + } + + return extractedContexts; + } + + internal static List? CreateSpanLinks(List producerContexts, bool linksEnabled) + { + if (!linksEnabled || producerContexts.Count == 0) + { + return null; + } + + var links = new List(producerContexts.Count); + foreach (var producerContext in producerContexts) + { + if (producerContext.SpanContext is { } producerSpanContext) + { + links.Add(new SpanLink(producerSpanContext)); + } + } + + return links; + } + + /// + /// Reads the CloudEvent JSON object or array for the given binding from InputData. + /// The full CloudEvent JSON (including extension attributes) lives in InputData, not + /// TriggerMetadata (which only contains {"data": ...} for Event Grid). + /// + internal static Dictionary[] GetCloudEvents(T context, string? bindingName) + where T : IFunctionContext + { + if (StringUtil.IsNullOrEmpty(bindingName)) + { + return []; + } + + var bindingsFeature = FunctionBindingsCommon.GetBindingsFeature(context); + if (bindingsFeature is null) + { + return []; + } + + if (bindingsFeature.Value.InputData is null + || !bindingsFeature.Value.InputData.TryGetValue(bindingName!, out var inputDataObj) + || inputDataObj is not string inputDataJson) + { + return []; + } + + var firstNonWhitespaceIndex = 0; + while (firstNonWhitespaceIndex < inputDataJson.Length && char.IsWhiteSpace(inputDataJson[firstNonWhitespaceIndex])) + { + firstNonWhitespaceIndex++; + } + + if (firstNonWhitespaceIndex == inputDataJson.Length) + { + return []; + } + + if (inputDataJson[firstNonWhitespaceIndex] == '[') + { + return FunctionBindingsCommon.TryParseJson[]>(inputDataJson, out var cloudEventBatch) ? cloudEventBatch : []; + } + + return FunctionBindingsCommon.TryParseJson>(inputDataJson, out var cloudEventProps) ? [cloudEventProps] : []; + } + + private static PropagationContext CreateReceiveSpan(Tracer tracer, Dictionary[] cloudEvents, List producerContexts) + { + try + { + var tags = tracer.CurrentTraceSettings.Schema.Messaging.CreateAzureEventGridTags(SpanKinds.Consumer); + tags.MessagingOperation = "receive"; + + var links = CreateSpanLinks(producerContexts, tracer.Settings.AzureEventGridBatchLinksEnabled); + var (serviceName, serviceNameSource) = tracer.CurrentTraceSettings.Schema.Messaging.GetServiceNameMetadata(MessagingSchema.ServiceType.AzureEventGrid); + + using var scope = tracer.StartActiveInternal( + "azure_eventgrid.receive", + parent: SpanContext.None, + tags: tags, + serviceName: serviceName, + serviceNameSource: serviceNameSource, + links: links); + + var span = scope.Span; + span.Type = SpanTypes.Queue; + span.ResourceName = "eventgrid"; + + if (cloudEvents.Length == 1) + { + var cloudEventProps = cloudEvents[0]; + if (cloudEventProps.TryGetValue("id", out var idObj) && idObj is string id && id.Length > 0) + { + span.SetTag(Tags.MessagingMessageId, id); + } + } + else if (cloudEvents.Length > 1) + { + tags.MessagingBatchMessageCount = cloudEvents.Length.ToString(); + } + + tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId.AzureEventGrid); + + return new PropagationContext(span.Context, baggage: null); + } + catch (Exception ex) + { + Log.Error(ex, "Error creating Azure Event Grid receive span"); + return producerContexts.Count == 1 ? producerContexts[0] : default; + } + } + } +} + +#endif diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/FunctionBindingsCommon.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/FunctionBindingsCommon.cs new file mode 100644 index 000000000000..4e61c8708813 --- /dev/null +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/FunctionBindingsCommon.cs @@ -0,0 +1,66 @@ +// +// 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. +// + +#if !NETFRAMEWORK + +using System; +using System.Diagnostics.CodeAnalysis; +using Datadog.Trace.DuckTyping; +using Datadog.Trace.Logging; +using Datadog.Trace.Util.Json; + +#nullable enable + +namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Functions +{ + internal static class FunctionBindingsCommon + { + private const string BindingsFeatureTypeName = "Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature"; + + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(FunctionBindingsCommon)); + + internal static FunctionBindingsFeatureStruct? GetBindingsFeature(T context) + where T : IFunctionContext + { + if (context.Features is null) + { + return null; + } + + foreach (var feature in context.Features) + { + if (feature.Key.FullName == BindingsFeatureTypeName) + { + return feature.Value?.TryDuckCast(out var bindingsFeature) == true ? bindingsFeature : null; + } + } + + return null; + } + + internal static bool TryParseJson(object? jsonObject, [NotNullWhen(true)] out T? result) + where T : class + { + result = null; + if (jsonObject is not string jsonString) + { + return false; + } + + try + { + result = JsonHelper.DeserializeObject(jsonString); + return result is not null; + } + catch (Exception ex) + { + Log.Debug(ex, "Failed to parse JSON payload with length {PayloadLength}", jsonString.Length); + return false; + } + } + } +} + +#endif diff --git a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs index 82aa7c3c99c3..cf297b7bff5f 100644 --- a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs @@ -448,6 +448,10 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) = .WithKeys(ConfigurationKeys.AzureEventHubsBatchLinksEnabled) .AsBool(defaultValue: true); + AzureEventGridBatchLinksEnabled = config + .WithKeys(ConfigurationKeys.AzureEventGridBatchLinksEnabled) + .AsBool(defaultValue: true); + AgentFeaturePollingEnabled = config .WithKeys(ConfigurationKeys.AgentFeaturePollingEnabled) .AsBool(defaultValue: true); @@ -1137,6 +1141,12 @@ not null when string.Equals(value, "otlp", StringComparison.OrdinalIgnoreCase) = /// public bool AzureEventHubsBatchLinksEnabled { get; } + /// + /// Gets a value indicating whether span links should be created for Azure Event Grid batch operations. + /// + /// + public bool AzureEventGridBatchLinksEnabled { get; } + /// /// Gets a value indicating whether the agent discovery service is enabled. /// When disabled, the tracer will not query the agent for available endpoints. diff --git a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml index 6292ff2173cd..3989ab1e1ea7 100644 --- a/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml +++ b/tracer/src/Datadog.Trace/Configuration/supported-configurations.yaml @@ -2767,6 +2767,16 @@ supportedConfigurations: documentation: |- Configuration key for toggling span pointers on AWS requests. Default value is true + DD_TRACE_AZURE_EVENTGRID_BATCH_LINKS_ENABLED: + - implementation: A + scope: managed + type: boolean + default: 'true' + const_name: AzureEventGridBatchLinksEnabled + documentation: |- + Configuration key for enabling or disabling span links creation for Azure Event Grid batch operations. + Default value is true (enabled). + DD_TRACE_AZURE_EVENTHUBS_BATCH_LINKS_ENABLED: - implementation: A scope: managed diff --git a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs index da4066b3d9f7..d2040e66f36d 100644 --- a/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs +++ b/tracer/src/Datadog.Trace/Generated/net461/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs @@ -347,6 +347,13 @@ internal static partial class ConfigurationKeys /// public const string SpanPointersEnabled = "DD_TRACE_AWS_ADD_SPAN_POINTERS"; + /// + /// Configuration key for enabling or disabling span links creation for Azure Event Grid batch operations. + /// Default value is true (enabled). + /// + /// + public const string AzureEventGridBatchLinksEnabled = "DD_TRACE_AZURE_EVENTGRID_BATCH_LINKS_ENABLED"; + /// /// Configuration key for enabling or disabling span links creation for Azure EventHubs batch operations. /// When enabled, TryAdd spans are created and linked to the send span. diff --git a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs index da4066b3d9f7..d2040e66f36d 100644 --- a/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs +++ b/tracer/src/Datadog.Trace/Generated/net6.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs @@ -347,6 +347,13 @@ internal static partial class ConfigurationKeys /// public const string SpanPointersEnabled = "DD_TRACE_AWS_ADD_SPAN_POINTERS"; + /// + /// Configuration key for enabling or disabling span links creation for Azure Event Grid batch operations. + /// Default value is true (enabled). + /// + /// + public const string AzureEventGridBatchLinksEnabled = "DD_TRACE_AZURE_EVENTGRID_BATCH_LINKS_ENABLED"; + /// /// Configuration key for enabling or disabling span links creation for Azure EventHubs batch operations. /// When enabled, TryAdd spans are created and linked to the send span. diff --git a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs index da4066b3d9f7..d2040e66f36d 100644 --- a/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs +++ b/tracer/src/Datadog.Trace/Generated/netcoreapp3.1/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs @@ -347,6 +347,13 @@ internal static partial class ConfigurationKeys /// public const string SpanPointersEnabled = "DD_TRACE_AWS_ADD_SPAN_POINTERS"; + /// + /// Configuration key for enabling or disabling span links creation for Azure Event Grid batch operations. + /// Default value is true (enabled). + /// + /// + public const string AzureEventGridBatchLinksEnabled = "DD_TRACE_AZURE_EVENTGRID_BATCH_LINKS_ENABLED"; + /// /// Configuration key for enabling or disabling span links creation for Azure EventHubs batch operations. /// When enabled, TryAdd spans are created and linked to the send span. diff --git a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs index da4066b3d9f7..d2040e66f36d 100644 --- a/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs +++ b/tracer/src/Datadog.Trace/Generated/netstandard2.0/Datadog.Trace.SourceGenerators/ConfigurationKeysGenerator/ConfigurationKeys.g.cs @@ -347,6 +347,13 @@ internal static partial class ConfigurationKeys /// public const string SpanPointersEnabled = "DD_TRACE_AWS_ADD_SPAN_POINTERS"; + /// + /// Configuration key for enabling or disabling span links creation for Azure Event Grid batch operations. + /// Default value is true (enabled). + /// + /// + public const string AzureEventGridBatchLinksEnabled = "DD_TRACE_AZURE_EVENTGRID_BATCH_LINKS_ENABLED"; + /// /// Configuration key for enabling or disabling span links creation for Azure EventHubs batch operations. /// When enabled, TryAdd spans are created and linked to the send span. diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AzureFunctionsMessagingTriggerTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AzureFunctionsMessagingTriggerTests.cs index ae51e922ecc9..146aa156e1d6 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AzureFunctionsMessagingTriggerTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/AzureFunctionsMessagingTriggerTests.cs @@ -16,6 +16,7 @@ using Azure.Messaging.ServiceBus; using Azure.Storage.Blobs; using Datadog.Trace.ClrProfiler.IntegrationTests.Azure; +using Datadog.Trace.Configuration; using Datadog.Trace.TestHelpers; using VerifyTests; using VerifyXunit; @@ -39,6 +40,7 @@ public class AzureFunctionsMessagingTriggerTests : AzureFunctionsTests private const string EventHubConsumerGroup = "cg1"; private const string TestIdEnvironmentVariable = "DD_AZURE_FUNCTIONS_MESSAGING_TEST_ID"; private const string TestModeEnvironmentVariable = "DD_AZURE_FUNCTIONS_MESSAGING_TEST_MODE"; + private const string EventGridFunctionsTopicEndpointEnvironmentVariable = "EVENTGRID_AZURE_FUNCTIONS_TOPIC_ENDPOINT"; private const string LocalServiceBusConnectionString = "Endpoint=sb://localhost:5672;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; private const string LocalEventHubsConnectionString = "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; private const string AzuriteAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="; @@ -51,7 +53,14 @@ public AzureFunctionsMessagingTriggerTests(ITestOutputHelper output) SetEnvironmentVariable("WEBSITE_SITE_NAME", nameof(AzureFunctionsMessagingTriggerTests)); SetEnvironmentVariable("ASB_CONNECTION_STRING", GetServiceBusConnectionString()); SetEnvironmentVariable("EVENTHUBS_CONNECTION_STRING", GetEventHubsConnectionString()); + SetEnvironmentVariable("EVENTGRID_TOPIC_ENDPOINT", GetEventGridTopicEndpoint()); + SetEnvironmentVariable("EVENTGRID_TOPIC_KEY", "test-key"); + SetEnvironmentVariable("DD_TRACE_AZUREEVENTGRID_ENABLED", "true"); SetEnvironmentVariable("AzureWebJobsStorage", GetAzuriteConnectionString()); + // Exclude the Event Grid emulator endpoint from HTTP client tracing to avoid an extra http.request span. + SetEnvironmentVariable( + "DD_TRACE_HTTP_CLIENT_EXCLUDED_URL_SUBSTRINGS", + ImmutableAzureAppServiceSettings.DefaultHttpClientExclusions + ", devstoreaccount1/azure-webjobs-hosts, samples-azure-functions-eventgrid-topic/api/events"); } private static int ExpectedFuncKillExitCode @@ -68,6 +77,7 @@ public async Task ServiceBusTrigger_SubmitsTrace() SetEnvironmentVariable("AzureFunctionsWebHost__hostid", CreateHostId(testId)); SetEnvironmentVariable("AzureWebJobs.ServiceBusTrigger.Disabled", "false"); SetEnvironmentVariable("AzureWebJobs.EventHubTrigger.Disabled", "true"); + SetEnvironmentVariable("AzureWebJobs.EventGridTrigger.Disabled", "true"); await using (var client = new ServiceBusClient(GetServiceBusConnectionString())) await using (var receiver = client.CreateReceiver(ServiceBusQueueName)) @@ -81,10 +91,14 @@ public async Task ServiceBusTrigger_SubmitsTrace() seedAsync: () => SeedViaHttpAsync("seed/servicebus"), expectedExitCode: ExpectedFuncKillExitCode)) { - // 7 spans total: 1 health-check ping + 6 meaningful spans + // Wait for at least 7 spans (1 health-check ping + 6 meaningful). var allSpans = await agent.WaitForSpansAsync(7, timeoutInMilliseconds: 30000, returnAllOperations: true); - // Filter out the health-check ping used to detect host readiness - var spans = allSpans.Where(s => s.Resource != "GET /admin/host/ping").ToImmutableList(); + var filteredSpans = allSpans.Where(s => s.Resource != "GET /admin/host/ping").ToImmutableList(); + var manualSpan = filteredSpans.FirstOrDefault(s => s.Name == "Manual inside ServiceBusTrigger"); + var sendSpan = filteredSpans.FirstOrDefault(s => s.Name == "azure_servicebus.send"); + var spans = filteredSpans + .Where(s => s.TraceId == manualSpan?.TraceId || s.TraceId == sendSpan?.TraceId) + .ToImmutableList(); var settings = GetMessagingTriggerSettings(); await VerifyHelper.VerifySpans(spans, settings) .UseFileName($"{nameof(AzureFunctionsMessagingTriggerTests)}.{nameof(ServiceBusTrigger_SubmitsTrace)}") @@ -103,6 +117,7 @@ public async Task EventHubTrigger_SubmitsTrace() SetEnvironmentVariable("AzureFunctionsWebHost__hostid", CreateHostId(testId)); SetEnvironmentVariable("AzureWebJobs.ServiceBusTrigger.Disabled", "true"); SetEnvironmentVariable("AzureWebJobs.EventHubTrigger.Disabled", "false"); + SetEnvironmentVariable("AzureWebJobs.EventGridTrigger.Disabled", "true"); // Seed happens inside the running app (HTTP trigger), so the event is always enqueued // after the app starts. Start reading from now to avoid stale events from prior runs. @@ -136,6 +151,44 @@ await VerifyHelper.VerifySpans(spans, settings) } } + [SkippableTheory] + [InlineData("EventGrid", "seed/eventgrid", "EventGridTrigger_SubmitsTrace")] + [InlineData("EventGridOutputBinding", "output/eventgrid", "EventGridOutputBinding_SubmitsTrace")] + public async Task EventGrid_SubmitsTrace(string mode, string route, string snapshotName) + { + Skip.If(EnvironmentHelper.IsAlpine(), "Azure Functions Core Tools are not installed in the Alpine integration test image."); + + var testId = CreateTestId(); + SetEnvironmentVariable(TestModeEnvironmentVariable, mode); + SetEnvironmentVariable(TestIdEnvironmentVariable, testId); + SetEnvironmentVariable("AzureFunctionsWebHost__hostid", CreateHostId(testId)); + SetEnvironmentVariable("AzureWebJobs.ServiceBusTrigger.Disabled", "true"); + SetEnvironmentVariable("AzureWebJobs.EventHubTrigger.Disabled", "true"); + SetEnvironmentVariable("AzureWebJobs.EventGridTrigger.Disabled", "false"); + + using var agent = EnvironmentHelper.GetMockAgent(useTelemetry: true); + using (await RunAzureFunctionAndWaitForExit( + agent, + seedAsync: () => SeedViaHttpAsync(route), + expectedExitCode: ExpectedFuncKillExitCode)) + { + // Wait for the producer trace and the trace created when the emulator delivers the event. + var allSpans = await agent.WaitForSpansAsync(7, timeoutInMilliseconds: 30000, returnAllOperations: true); + // Keep only those two traces. Matching on the per-run test id (the CloudEvent id, surfaced as + // messaging.message_id) ignores events the emulator redelivers to the fixed 7071 webhook. + var filteredSpans = allSpans.Where(s => s.Resource != "GET /admin/host/ping").ToImmutableList(); + var sendSpan = filteredSpans.FirstOrDefault(s => s.Name == "azure_eventgrid.send" && s.GetTag("messaging.message_id") == testId); + var receiveSpan = filteredSpans.FirstOrDefault(s => s.Name == "azure_eventgrid.receive" && s.GetTag("messaging.message_id") == testId); + var spans = filteredSpans + .Where(s => s.TraceId == sendSpan?.TraceId || s.TraceId == receiveSpan?.TraceId) + .ToImmutableList(); + var settings = GetMessagingTriggerSettings(); + await VerifyHelper.VerifySpans(spans, settings) + .UseFileName($"{nameof(AzureFunctionsMessagingTriggerTests)}.{snapshotName}") + .DisableRequireUniquePrefix(); + } + } + private static VerifySettings GetMessagingTriggerSettings() { var settings = VerifyHelper.GetSpanVerifierSettings(); @@ -143,8 +196,10 @@ private static VerifySettings GetMessagingTriggerSettings() // Normalize emulator hostnames so snapshots are consistent across local and CI (Docker) environments settings.AddSimpleScrubber("network.destination.name: azure-eventhubs-emulator", "network.destination.name: localhost"); settings.AddSimpleScrubber("network.destination.name: azureservicebus-emulator", "network.destination.name: localhost"); + settings.AddSimpleScrubber("network.destination.name: azure-eventgrid-emulator", "network.destination.name: localhost"); settings.AddSimpleScrubber("server.address: azure-eventhubs-emulator", "server.address: localhost"); settings.AddSimpleScrubber("server.address: azureservicebus-emulator", "server.address: localhost"); + settings.AddSimpleScrubber("server.address: azure-eventgrid-emulator", "server.address: localhost"); // SpanLinks contain raw 128-bit trace IDs and trace state that change between runs settings.AddRegexScrubber(new Regex(@"TraceIdLow: \d+"), "TraceIdLow: 0"); settings.AddRegexScrubber(new Regex(@"TraceIdHigh: \d+"), "TraceIdHigh: 0"); @@ -220,6 +275,10 @@ private static string GetServiceBusConnectionString() private static string GetEventHubsConnectionString() => Environment.GetEnvironmentVariable("EVENTHUBS_CONNECTION_STRING") ?? LocalEventHubsConnectionString; + private static string GetEventGridTopicEndpoint() + => Environment.GetEnvironmentVariable(EventGridFunctionsTopicEndpointEnvironmentVariable) + ?? "http://localhost:6500/samples-azure-functions-eventgrid-topic/api/events"; + private static string GetAzuriteConnectionString() { var host = UseDockerHostnames() ? "azurite" : "127.0.0.1"; diff --git a/tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommonTests.cs b/tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommonTests.cs index 452a76443567..f30aeea516c3 100644 --- a/tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommonTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommonTests.cs @@ -117,7 +117,7 @@ private static MockFunctionContext CreateMockFunctionContext(string propertyKey, } // This duck types with tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/Isolated/IFunctionContext.cs - private class MockFunctionContext : IFunctionContext + internal class MockFunctionContext : IFunctionContext { public FunctionDefinitionStruct FunctionDefinition { get; set; } @@ -127,7 +127,7 @@ private class MockFunctionContext : IFunctionContext } // This duck types with tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/Isolated/GrpcBindingsFeatureStruct.cs - private class MockBindingsFeature + internal class MockBindingsFeature { public IDictionary? TriggerMetadata { get; set; } diff --git a/tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommonTests.cs b/tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommonTests.cs new file mode 100644 index 000000000000..68a22946ce11 --- /dev/null +++ b/tracer/test/Datadog.Trace.Tests/ClrProfiler/AutoInstrumentation/Azure/Functions/EventGridFunctionsCommonTests.cs @@ -0,0 +1,114 @@ +// +// 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. +// + +#nullable enable + +#if !NETFRAMEWORK + +using System; +using System.Collections.Generic; +using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Functions; +using Datadog.Trace.Propagators; +using Datadog.Trace.Vendors.Newtonsoft.Json; +using FluentAssertions; +using Xunit; +using MockBindingsFeature = Datadog.Trace.Tests.ClrProfiler.AutoInstrumentation.Azure.Functions.AzureFunctionsCommonTests.MockBindingsFeature; +using MockFunctionContext = Datadog.Trace.Tests.ClrProfiler.AutoInstrumentation.Azure.Functions.AzureFunctionsCommonTests.MockFunctionContext; + +namespace Datadog.Trace.Tests.ClrProfiler.AutoInstrumentation.Azure.Functions +{ + public class EventGridFunctionsCommonTests + { + [Fact] + public void ExtractPropagatedContextsFromEventGrid_SingleCloudEvent_ExtractsContext() + { + var cloudEvent = new Dictionary + { + ["specversion"] = "1.0", + ["type"] = "test.type", + ["source"] = "/test/source", + ["id"] = "test-id", + ["traceparent"] = $"00-{1:x32}-{1:x16}-01", + ["tracestate"] = "dd=s:1", + ["baggage"] = "user.id=123", + }; + var context = CreateMockFunctionContext("myEvent", JsonConvert.SerializeObject(cloudEvent)); + + var extractedContexts = ExtractPropagatedContexts(context, "myEvent"); + + extractedContexts.Should().ContainSingle(); + extractedContexts[0].SpanContext.Should().NotBeNull(); + extractedContexts[0].Baggage.Should().NotBeNull(); + extractedContexts[0].Baggage!["user.id"].Should().Be("123"); + } + + [Fact] + public void ExtractPropagatedContextsFromEventGrid_SingleCloudEvent_TraceparentOnly() + { + var cloudEvent = new Dictionary + { + ["specversion"] = "1.0", + ["type"] = "test.type", + ["source"] = "/test/source", + ["id"] = "test-id", + ["traceparent"] = $"00-{1:x32}-{1:x16}-01", + }; + var context = CreateMockFunctionContext("myEvent", JsonConvert.SerializeObject(cloudEvent)); + + var extractedContexts = ExtractPropagatedContexts(context, "myEvent"); + + extractedContexts.Should().ContainSingle(); + extractedContexts[0].SpanContext.Should().NotBeNull(); + } + + [Fact] + public void ExtractPropagatedContextsFromEventGrid_NoTraceContext_ReturnsEmpty() + { + var cloudEvent = new Dictionary + { + ["specversion"] = "1.0", + ["type"] = "test.type", + ["source"] = "/test/source", + ["id"] = "test-id", + }; + var context = CreateMockFunctionContext("myEvent", JsonConvert.SerializeObject(cloudEvent)); + + var extractedContexts = ExtractPropagatedContexts(context, "myEvent"); + + extractedContexts.Should().BeEmpty(); + } + + private static List ExtractPropagatedContexts(MockFunctionContext context, string? bindingName) + { + var cloudEvents = EventGridFunctionsCommon.GetCloudEvents(context, bindingName); + return EventGridFunctionsCommon.ExtractPropagatedContexts(cloudEvents); + } + + private static MockFunctionContext CreateMockFunctionContext(string bindingName, string inputDataJson) + { + var inputData = new Dictionary + { + [bindingName] = inputDataJson + }; + + var bindingsFeature = new MockBindingsFeature + { + InputData = inputData + }; + + var features = new List> + { + new(typeof(Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature), bindingsFeature) + }; + + return new MockFunctionContext + { + Features = features + }; + } + } +} + +#endif diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/ConfigurationSourceTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/ConfigurationSourceTests.cs index af87926a762e..d3129d2b8b4e 100644 --- a/tracer/test/Datadog.Trace.Tests/Configuration/ConfigurationSourceTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Configuration/ConfigurationSourceTests.cs @@ -91,6 +91,7 @@ public ConfigurationSourceTests() yield return (s => s.TraceId128BitGenerationEnabled, true); yield return (s => s.TraceId128BitLoggingEnabled, true); + yield return (s => s.AzureEventGridBatchLinksEnabled, true); yield return (s => s.AgentFeaturePollingEnabled, true); } @@ -118,6 +119,8 @@ public ConfigurationSourceTests() yield return (ConfigurationKeys.ServiceName, "web-service", s => s.Manager.InitialMutableSettings.ServiceName, "web-service"); yield return ("DD_SERVICE_NAME", "web-service", s => s.Manager.InitialMutableSettings.ServiceName, "web-service"); + yield return (ConfigurationKeys.AzureEventGridBatchLinksEnabled, "false", s => s.AzureEventGridBatchLinksEnabled, false); + yield return (ConfigurationKeys.DisabledIntegrations, "integration1;integration2;;INTEGRATION2", s => s.Manager.InitialMutableSettings.DisabledIntegrationNames.Count, 3); // The OpenTelemetry integration is disabled by defau)t yield return (ConfigurationKeys.GlobalTags, "k1:v1, k2:v2", s => s.Manager.InitialMutableSettings.GlobalTags, TagsK1V1K2V2); diff --git a/tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridOutputBinding_SubmitsTrace.verified.txt b/tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridOutputBinding_SubmitsTrace.verified.txt new file mode 100644 index 000000000000..0ab1384f6d77 --- /dev/null +++ b/tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridOutputBinding_SubmitsTrace.verified.txt @@ -0,0 +1,187 @@ +[ + { + TraceId: Id_1, + SpanId: Id_2, + Name: azure_eventgrid.receive, + Resource: eventgrid, + Service: AzureFunctionsMessagingTriggerTests-azureeventgrid, + Type: queue, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureEventGrid, + env: integration_tests, + language: dotnet, + messaging.message_id: Guid_1, + messaging.operation: receive, + messaging.system: eventgrid, + runtime-id: Guid_2, + span.kind: consumer, + _dd.base_service: AzureFunctionsMessagingTriggerTests, + _dd.svc_src: azureeventgrid + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + }, + SpanLinks: [ + { + TraceIdLow: 0, + TraceIdHigh: 0, + SpanId: Id_3, + TraceFlags: 0, + TraceState: scrubbed + } + ] + }, + { + TraceId: Id_1, + SpanId: Id_4, + Name: azure_functions.invoke, + Resource: EventGrid EventGridTrigger, + Service: AzureFunctionsMessagingTriggerTests, + Type: serverless, + ParentId: Id_2, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.function.method: Samples.AzureFunctions.Messaging.MessagingTriggers.EventGridTrigger, + aas.function.name: EventGridTrigger, + aas.function.trigger: EventGrid, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureFunctions, + env: integration_tests, + language: dotnet, + runtime-id: Guid_2, + span.kind: server + }, + Metrics: { + _dd.top_level: 1.0, + _sampling_priority_v1: 1.0 + } + }, + { + TraceId: Id_1, + SpanId: Id_5, + Name: Manual inside EventGridTrigger, + Resource: Manual inside EventGridTrigger, + Service: AzureFunctionsMessagingTriggerTests, + ParentId: Id_4, + Tags: { + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + env: integration_tests, + language: dotnet + } + }, + { + TraceId: Id_6, + SpanId: Id_7, + Name: azure_functions.invoke, + Resource: POST /api/output/eventgrid, + Service: AzureFunctionsMessagingTriggerTests, + Type: serverless, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.function.binding: Microsoft.Azure.WebJobs.Host.Executors.BindingSource, + aas.function.name: EventGridOutputBinding, + aas.function.trigger: Http, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureFunctions, + env: integration_tests, + http.method: POST, + http.request.headers.host: localhost:00000, + http.status_code: 200, + http.url: http://localhost:00000/api/output/eventgrid, + language: dotnet, + runtime-id: Guid_3, + span.kind: server + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + } + }, + { + TraceId: Id_6, + SpanId: Id_8, + Name: azure_functions.invoke, + Resource: Http EventGridOutputBinding, + Service: AzureFunctionsMessagingTriggerTests, + Type: serverless, + ParentId: Id_7, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.function.method: Samples.AzureFunctions.Messaging.MessagingTriggers.EventGridOutputBinding, + aas.function.name: EventGridOutputBinding, + aas.function.trigger: Http, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureFunctions, + env: integration_tests, + language: dotnet, + runtime-id: Guid_2, + span.kind: server + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + } + }, + { + TraceId: Id_6, + SpanId: Id_3, + Name: azure_eventgrid.send, + Resource: eventgrid, + Service: AzureFunctionsMessagingTriggerTests-azureeventgrid, + Type: queue, + ParentId: Id_7, + Tags: { + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureEventGrid, + env: integration_tests, + language: dotnet, + messaging.message_id: Guid_1, + messaging.operation: send, + messaging.system: eventgrid, + network.destination.name: localhost, + network.destination.port: 6500, + runtime-id: Guid_3, + span.kind: producer, + _dd.base_service: AzureFunctionsMessagingTriggerTests, + _dd.svc_src: azureeventgrid + }, + Metrics: { + _dd.top_level: 1.0 + } + } +] \ No newline at end of file diff --git a/tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridTrigger_SubmitsTrace.verified.txt b/tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridTrigger_SubmitsTrace.verified.txt new file mode 100644 index 000000000000..f7c4c910eba9 --- /dev/null +++ b/tracer/test/snapshots/AzureFunctionsMessagingTriggerTests.EventGridTrigger_SubmitsTrace.verified.txt @@ -0,0 +1,187 @@ +[ + { + TraceId: Id_1, + SpanId: Id_2, + Name: azure_eventgrid.receive, + Resource: eventgrid, + Service: AzureFunctionsMessagingTriggerTests-azureeventgrid, + Type: queue, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureEventGrid, + env: integration_tests, + language: dotnet, + messaging.message_id: Guid_1, + messaging.operation: receive, + messaging.system: eventgrid, + runtime-id: Guid_2, + span.kind: consumer, + _dd.base_service: AzureFunctionsMessagingTriggerTests, + _dd.svc_src: azureeventgrid + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + }, + SpanLinks: [ + { + TraceIdLow: 0, + TraceIdHigh: 0, + SpanId: Id_3, + TraceFlags: 0, + TraceState: scrubbed + } + ] + }, + { + TraceId: Id_1, + SpanId: Id_4, + Name: azure_functions.invoke, + Resource: EventGrid EventGridTrigger, + Service: AzureFunctionsMessagingTriggerTests, + Type: serverless, + ParentId: Id_2, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.function.method: Samples.AzureFunctions.Messaging.MessagingTriggers.EventGridTrigger, + aas.function.name: EventGridTrigger, + aas.function.trigger: EventGrid, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureFunctions, + env: integration_tests, + language: dotnet, + runtime-id: Guid_2, + span.kind: server + }, + Metrics: { + _dd.top_level: 1.0, + _sampling_priority_v1: 1.0 + } + }, + { + TraceId: Id_1, + SpanId: Id_5, + Name: Manual inside EventGridTrigger, + Resource: Manual inside EventGridTrigger, + Service: AzureFunctionsMessagingTriggerTests, + ParentId: Id_4, + Tags: { + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + env: integration_tests, + language: dotnet + } + }, + { + TraceId: Id_6, + SpanId: Id_7, + Name: azure_functions.invoke, + Resource: POST /api/seed/eventgrid, + Service: AzureFunctionsMessagingTriggerTests, + Type: serverless, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.function.binding: Microsoft.Azure.WebJobs.Host.Executors.BindingSource, + aas.function.name: SeedEventGridEvent, + aas.function.trigger: Http, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureFunctions, + env: integration_tests, + http.method: POST, + http.request.headers.host: localhost:00000, + http.status_code: 200, + http.url: http://localhost:00000/api/seed/eventgrid, + language: dotnet, + runtime-id: Guid_3, + span.kind: server + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + } + }, + { + TraceId: Id_6, + SpanId: Id_8, + Name: azure_functions.invoke, + Resource: Http SeedEventGridEvent, + Service: AzureFunctionsMessagingTriggerTests, + Type: serverless, + ParentId: Id_7, + Tags: { + aas.environment.extension_version: unknown, + aas.environment.instance_id: unknown, + aas.environment.instance_name: IntegrationTestHost, + aas.environment.os: unknown, + aas.environment.runtime: .NET, + aas.function.method: Samples.AzureFunctions.Messaging.MessagingTriggers.SeedEventGridEvent, + aas.function.name: SeedEventGridEvent, + aas.function.trigger: Http, + aas.site.kind: functionapp, + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureFunctions, + env: integration_tests, + language: dotnet, + runtime-id: Guid_2, + span.kind: server + }, + Metrics: { + process_id: 0, + _dd.top_level: 1.0, + _dd.tracer_kr: 1.0, + _sampling_priority_v1: 1.0 + } + }, + { + TraceId: Id_6, + SpanId: Id_3, + Name: azure_eventgrid.send, + Resource: eventgrid, + Service: AzureFunctionsMessagingTriggerTests-azureeventgrid, + Type: queue, + ParentId: Id_8, + Tags: { + aas.site.name: AzureFunctionsMessagingTriggerTests, + aas.site.type: function, + component: AzureEventGrid, + env: integration_tests, + language: dotnet, + messaging.message_id: Guid_1, + messaging.operation: send, + messaging.system: eventgrid, + network.destination.name: localhost, + network.destination.port: 6500, + runtime-id: Guid_2, + span.kind: producer, + _dd.base_service: AzureFunctionsMessagingTriggerTests, + _dd.svc_src: azureeventgrid + }, + Metrics: { + _dd.top_level: 1.0 + } + } +] \ No newline at end of file diff --git a/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/MessagingTriggers.cs b/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/MessagingTriggers.cs index 4959d09ed8ff..3b431b71379a 100644 --- a/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/MessagingTriggers.cs +++ b/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/MessagingTriggers.cs @@ -1,6 +1,9 @@ using System.Diagnostics; using System.Net; using System.Text; +using Azure; +using Azure.Messaging; +using Azure.Messaging.EventGrid; using Azure.Messaging.EventHubs; using Azure.Messaging.EventHubs.Producer; using Azure.Messaging.ServiceBus; @@ -63,6 +66,21 @@ public Task EventHubTrigger( return Task.CompletedTask; } + [Function("EventGridTrigger")] + public Task EventGridTrigger([EventGridTrigger] CloudEvent cloudEvent) + { + using (SampleHelpers.CreateScope("Manual inside EventGridTrigger")) + { + _logger.LogInformation( + "Processed Event Grid event {Id}: {Type}", + cloudEvent.Id, + cloudEvent.Type); + } + + ScheduleShutdown(); + return Task.CompletedTask; + } + [Function("SeedServiceBusMessage")] public async Task SeedServiceBusMessage( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "seed/servicebus")] HttpRequestData req) @@ -96,6 +114,41 @@ public async Task SeedEventHubEvent( return req.CreateResponse(HttpStatusCode.OK); } + [Function("SeedEventGridEvent")] + public async Task SeedEventGridEvent( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "seed/eventgrid")] HttpRequestData req) + { + var testId = Environment.GetEnvironmentVariable(TestIdEnvironmentVariable) ?? string.Empty; + var endpoint = Environment.GetEnvironmentVariable("EVENTGRID_TOPIC_ENDPOINT")!; + + var client = new EventGridPublisherClient(new Uri(endpoint), new AzureKeyCredential("test-key")); + + var cloudEvent = new CloudEvent( + source: "/Samples.AzureFunctions.Messaging/eventgrid", + type: nameof(EventGridTrigger), + jsonSerializableData: new { message = $"Seeded event {testId}", testId }) + { + Id = testId, + }; + await client.SendEventAsync(cloudEvent); + return req.CreateResponse(HttpStatusCode.OK); + } + + [Function("EventGridOutputBinding")] + [EventGridOutput(TopicEndpointUri = "EVENTGRID_TOPIC_ENDPOINT", TopicKeySetting = "EVENTGRID_TOPIC_KEY")] + public CloudEvent EventGridOutputBinding( + [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "output/eventgrid")] HttpRequestData req) + { + var testId = Environment.GetEnvironmentVariable(TestIdEnvironmentVariable) ?? string.Empty; + return new CloudEvent( + source: "/Samples.AzureFunctions.Messaging/eventgrid-output-binding", + type: nameof(EventGridTrigger), + jsonSerializableData: new { message = $"Output binding event {testId}", testId }) + { + Id = testId, + }; + } + private void ScheduleShutdown() { if (Interlocked.Exchange(ref _shutdownStarted, 1) == 1) diff --git a/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/Samples.AzureFunctions.V4Isolated.Messaging.csproj b/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/Samples.AzureFunctions.V4Isolated.Messaging.csproj index 02d1ec21d075..2af5be88d307 100644 --- a/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/Samples.AzureFunctions.V4Isolated.Messaging.csproj +++ b/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/Samples.AzureFunctions.V4Isolated.Messaging.csproj @@ -7,9 +7,11 @@ $(NoWarn);NU1510 + + diff --git a/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/local.settings.json b/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/local.settings.json index 1ea2cf4ffe18..847c7541fa0d 100644 --- a/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/local.settings.json +++ b/tracer/test/test-applications/azure-functions/Samples.AzureFunctions.V4Isolated.Messaging/local.settings.json @@ -4,6 +4,8 @@ "AzureWebJobsStorage": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;", "ASB_CONNECTION_STRING": "Endpoint=sb://localhost:5672;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;", "EVENTHUBS_CONNECTION_STRING": "Endpoint=sb://localhost:5673;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;", + "EVENTGRID_TOPIC_ENDPOINT": "http://localhost:6500/samples-azure-functions-eventgrid-topic/api/events", + "EVENTGRID_TOPIC_KEY": "test-key", "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated" } }