Skip to content

Commit 4e4e326

Browse files
[Azure Event Grid] Add Azure Functions support
1 parent deb530a commit 4e4e326

15 files changed

Lines changed: 1046 additions & 97 deletions

File tree

.azure-pipelines/ultimate-pipeline.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,6 +2412,9 @@ stages:
24122412
SampleName: $(IntegrationTestSampleName)
24132413
DD_LOGGER_DD_API_KEY: $(ddApiKey)
24142414
COMPOSE_PROFILES: group$(dockerGroup)
2415+
# Event Grid emulator (group2) pushes to the Functions host webhook; in CI the func host runs inside
2416+
# the IntegrationTests container, reachable by sibling containers as "integrationtests".
2417+
EVENTGRID_WEBHOOK_HOST: integrationtests
24152418
displayName: docker-compose build IntegrationTests and run StartDependencies (Group $(dockerGroup))
24162419
retryCountOnTaskFailure: 5
24172420
@@ -2431,6 +2434,7 @@ stages:
24312434
DD_LOGGER_DD_API_KEY: $(ddApiKey)
24322435
baseImage: $(baseImage) # for interpolation in the docker-compose file
24332436
COMPOSE_PROFILES: group$(dockerGroup)
2437+
EVENTGRID_WEBHOOK_HOST: integrationtests # keep the emulator's webhook host stable if compose re-evaluates
24342438
24352439
- script: docker-compose -f docker-compose.yml -p $(DockerComposeProjectName)-g$(dockerGroup) logs
24362440
displayName: docker-compose logs

docker-compose.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ services:
280280
- "127.0.0.1:6500:6500"
281281
volumes:
282282
- ./docker/eventgrid-emulator-config.json:/app/appsettings.json:ro
283+
environment:
284+
# Override the topic's subscriber webhook (ASP.NET array config). The Azure Functions host that
285+
# receives the pushed event is reachable at a different host depending on where it runs: inside the
286+
# IntegrationTests container in CI (EVENTGRID_WEBHOOK_HOST=integrationtests) vs on the Docker host
287+
# locally (host.docker.internal). The emulator adds the aeg-event-type header and forwards the CloudEvent.
288+
- Topics__samples-azure-functions-eventgrid-topic__0=http://${EVENTGRID_WEBHOOK_HOST:-host.docker.internal}:7071/runtime/webhooks/eventgrid?functionName=EventGridTrigger
289+
extra_hosts:
290+
- "host.docker.internal:host-gateway"
283291

284292
cosmosdb-emulator:
285293
image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview@sha256:54d7bc334494c50cea867c270880671a7db080626a9732832b34c0d69342f9b0
@@ -465,6 +473,7 @@ services:
465473
- ASB_CONNECTION_STRING=Endpoint=sb://azureservicebus-emulator:5672;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;
466474
- EVENTHUBS_CONNECTION_STRING=Endpoint=sb://azure-eventhubs-emulator:5672;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;
467475
- EVENTGRID_TOPIC_ENDPOINT=http://azure-eventgrid-emulator:6500/samples-eventgrid-topic/api/events
476+
- EVENTGRID_AZURE_FUNCTIONS_TOPIC_ENDPOINT=http://azure-eventgrid-emulator:6500/samples-azure-functions-eventgrid-topic/api/events
468477
- COSMOSDB_ENDPOINT=https://cosmosdb-emulator:8081
469478
- TEST_AGENT_HOST=test-agent
470479
- CONTAINER_HOSTNAME=http://integrationtests
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"Topics": {
3-
"samples-eventgrid-topic": []
3+
"samples-eventgrid-topic": [],
4+
"samples-azure-functions-eventgrid-topic": []
45
}
56
}

tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Azure/Functions/AzureFunctionsCommon.cs

Lines changed: 15 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System;
99
using System.Collections;
1010
using System.Collections.Generic;
11-
using System.Diagnostics.CodeAnalysis;
1211
using System.Linq;
1312
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.Shared;
1413
using Datadog.Trace.ClrProfiler.AutoInstrumentation.Proxy;
@@ -20,9 +19,6 @@
2019
using Datadog.Trace.PlatformHelpers;
2120
using Datadog.Trace.Propagators;
2221
using Datadog.Trace.Tagging;
23-
using Datadog.Trace.Util;
24-
using Datadog.Trace.Util.Json;
25-
using Datadog.Trace.Vendors.Newtonsoft.Json;
2622
using Datadog.Trace.Vendors.Serilog.Events;
2723

2824
#nullable enable
@@ -314,6 +310,12 @@ _ when type.StartsWith("eventGrid", StringComparison.OrdinalIgnoreCase) => "Even
314310
case "EventHub" when tracer.CurrentTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AzureEventHubs):
315311
extractedContext = ExtractPropagatedContextFromMessaging(functionContext, "Properties", "PropertiesArray").MergeBaggageInto(Baggage.Current);
316312
break;
313+
314+
case "EventGrid" when tracer.CurrentTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AzureEventGrid, defaultValue: false):
315+
{
316+
extractedContext = EventGridFunctionsCommon.CreateReceiveSpanContext(tracer, functionContext, entry.Key as string, Baggage.Current);
317+
break;
318+
}
317319
}
318320

319321
break;
@@ -476,30 +478,21 @@ private static PropagationContext ExtractPropagatedContextFromHttp<T>(T function
476478
// directly from the grpc call instead. This is... interesting. It
477479
// is effectively doing the equivalent of context.GetHttpRequestDataAsync() which is
478480
// the suggested approach in the docs.
479-
if (functionContext.Features is null || string.IsNullOrEmpty(bindingName))
481+
if (string.IsNullOrEmpty(bindingName))
480482
{
481483
return default;
482484
}
483485

484486
try
485487
{
486-
object? feature = null;
487-
foreach (var keyValuePair in functionContext.Features)
488-
{
489-
if (keyValuePair.Key.FullName?.Equals("Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature") == true)
490-
{
491-
feature = keyValuePair.Value;
492-
break;
493-
}
494-
}
495-
496-
if (feature is null || !feature.TryDuckCast<FunctionBindingsFeatureStruct>(out var bindingFeature))
488+
var bindingsFeature = FunctionBindingsCommon.GetBindingsFeature(functionContext);
489+
if (bindingsFeature is null)
497490
{
498491
return default;
499492
}
500493

501-
if (bindingFeature.InputData is null
502-
|| !bindingFeature.InputData.TryGetValue(bindingName!, out var requestDataObject)
494+
if (bindingsFeature.Value.InputData is null
495+
|| !bindingsFeature.Value.InputData.TryGetValue(bindingName!, out var requestDataObject)
503496
|| requestDataObject is null)
504497
{
505498
return default;
@@ -524,8 +517,8 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging<T>(T co
524517
{
525518
try
526519
{
527-
var bindingsFeature = GetFeatureFromContext<T, FunctionBindingsFeatureStruct>(context, "Microsoft.Azure.Functions.Worker.Context.Features.IFunctionBindingsFeature");
528-
if (bindingsFeature == null)
520+
var bindingsFeature = FunctionBindingsCommon.GetBindingsFeature(context);
521+
if (bindingsFeature is null)
529522
{
530523
return default;
531524
}
@@ -535,7 +528,7 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging<T>(T co
535528

536529
// Extract from single message properties
537530
if (triggerMetadata?.TryGetValue(singlePropertyKey, out var singlePropsObj) == true &&
538-
TryParseJson<Dictionary<string, object>>(singlePropsObj, out var singleProps) && singleProps != null)
531+
FunctionBindingsCommon.TryParseJson<Dictionary<string, object>>(singlePropsObj, out var singleProps))
539532
{
540533
var singleContext = Shared.AzureMessagingCommon.ExtractContext(singleProps);
541534
if (singleContext.SpanContext != null)
@@ -546,7 +539,7 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging<T>(T co
546539

547540
// Extract from batch properties array
548541
if (triggerMetadata?.TryGetValue(batchPropertyKey, out var arrayPropsObj) == true &&
549-
TryParseJson<Dictionary<string, object>[]>(arrayPropsObj, out var propsArray) && propsArray != null)
542+
FunctionBindingsCommon.TryParseJson<Dictionary<string, object>[]>(arrayPropsObj, out var propsArray))
550543
{
551544
foreach (var props in propsArray)
552545
{
@@ -580,27 +573,6 @@ internal static PropagationContext ExtractPropagatedContextFromMessaging<T>(T co
580573
}
581574
}
582575

583-
private static bool TryParseJson<T>(object? jsonObj, [NotNullWhen(true)] out T? result)
584-
where T : class
585-
{
586-
result = null;
587-
if (jsonObj is not string jsonString)
588-
{
589-
return false;
590-
}
591-
592-
try
593-
{
594-
result = JsonHelper.DeserializeObject<T>(jsonString);
595-
return result != null;
596-
}
597-
catch (Exception ex)
598-
{
599-
Log.Debug(ex, "Failed to parse JSON: {Json}", jsonString);
600-
return false;
601-
}
602-
}
603-
604576
// Checks if all SpanContexts are identical (ignores baggage)
605577
private static bool AreAllSpanContextsIdentical(List<PropagationContext> contexts)
606578
{
@@ -615,26 +587,6 @@ private static bool AreAllSpanContextsIdentical(List<PropagationContext> context
615587
ctx.SpanContext.TraceId128 == first!.TraceId128 &&
616588
ctx.SpanContext.SpanId == first.SpanId);
617589
}
618-
619-
private static TFeature? GetFeatureFromContext<T, TFeature>(T context, string featureTypeName)
620-
where T : IFunctionContext
621-
where TFeature : struct
622-
{
623-
if (context.Features == null)
624-
{
625-
return null;
626-
}
627-
628-
foreach (var kvp in context.Features)
629-
{
630-
if (kvp.Key.FullName == featureTypeName)
631-
{
632-
return kvp.Value?.TryDuckCast<TFeature>(out var feature) == true ? feature : null;
633-
}
634-
}
635-
636-
return null;
637-
}
638590
}
639591
}
640592

0 commit comments

Comments
 (0)