Skip to content

Commit 1a3cf51

Browse files
[Azure Event Grid] Add contract and core behavior
1 parent 5c1da1a commit 1a3cf51

46 files changed

Lines changed: 2604 additions & 469 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
// <copyright file="EventGridCommon.cs" company="Datadog">
2+
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
4+
// </copyright>
5+
6+
#nullable enable
7+
8+
using System;
9+
using System.Collections;
10+
using System.Collections.Generic;
11+
using System.Reflection;
12+
using Datadog.Trace.ClrProfiler.CallTarget;
13+
using Datadog.Trace.Configuration;
14+
using Datadog.Trace.Configuration.Schema;
15+
using Datadog.Trace.DuckTyping;
16+
using Datadog.Trace.Logging;
17+
using Datadog.Trace.Propagators;
18+
using Datadog.Trace.Tagging;
19+
20+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.EventGrid;
21+
22+
internal static class EventGridCommon
23+
{
24+
private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(EventGridCommon));
25+
26+
internal static CallTargetState CreateProducerSpan<TTarget, TEvents>(TTarget instance, ref TEvents events, bool injectContext, string? destinationNameOverride = null)
27+
{
28+
var tracer = Tracer.Instance;
29+
if (!tracer.CurrentTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AzureEventGrid, defaultValue: false))
30+
{
31+
return CallTargetState.GetDefault();
32+
}
33+
34+
// The Functions host loads the Event Grid extension in a separate AssemblyLoadContext. Accessing
35+
// this private field through a duck-type proxy can fail with MissingFieldException even though the
36+
// field exists, so retrieve the field value from the concrete target type before duck casting it.
37+
IRequestUriBuilder? uriBuilder = null;
38+
if ((object?)instance is { } target)
39+
{
40+
uriBuilder = UriBuilderFieldCache<TTarget>.Field?.GetValue(target)?.DuckCast<IRequestUriBuilder>();
41+
}
42+
43+
var host = uriBuilder?.Host;
44+
var port = uriBuilder?.Port ?? -1;
45+
var destinationName = destinationNameOverride ?? GetTopicFromHost(host);
46+
return CreateProducerSpan(tracer, destinationName, host, port, ref events, injectContext);
47+
}
48+
49+
internal static CallTargetState CreateNamespaceProducerSpanForEvent<TTarget>(TTarget instance, object? cloudEvent)
50+
where TTarget : IEventGridSenderClient
51+
{
52+
var tracer = Tracer.Instance;
53+
if (!tracer.CurrentTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AzureEventGrid, defaultValue: false))
54+
{
55+
return CallTargetState.GetDefault();
56+
}
57+
58+
var endpoint = instance.Endpoint;
59+
return CreateProducerSpan(tracer, instance.TopicName, endpoint?.Host, endpoint?.Port ?? -1, events: null, cloudEvent);
60+
}
61+
62+
internal static CallTargetState CreateNamespaceProducerSpanForEvents<TTarget, TEvents>(TTarget instance, ref TEvents cloudEvents)
63+
where TTarget : IEventGridSenderClient
64+
{
65+
var tracer = Tracer.Instance;
66+
if (!tracer.CurrentTraceSettings.Settings.IsIntegrationEnabled(IntegrationId.AzureEventGrid, defaultValue: false))
67+
{
68+
return CallTargetState.GetDefault();
69+
}
70+
71+
var endpoint = instance.Endpoint;
72+
return CreateProducerSpan(tracer, instance.TopicName, endpoint?.Host, endpoint?.Port ?? -1, ref cloudEvents, injectContext: true);
73+
}
74+
75+
private static CallTargetState CreateProducerSpan<TEvents>(Tracer tracer, string? destinationName, string? host, int port, ref TEvents events, bool injectContext)
76+
{
77+
var enumerable = events as IEnumerable;
78+
var state = CreateProducerSpan(tracer, destinationName, host, port, enumerable, singleEvent: null);
79+
if (state.Scope is not { } scope || enumerable is null)
80+
{
81+
return state;
82+
}
83+
84+
try
85+
{
86+
var observer = new EventGridEnumerableObserver(scope, enumerable is ICollection collection ? collection.Count : null, injectContext);
87+
events = EventGridObservingEnumerable.Wrap(events, observer);
88+
}
89+
catch (Exception ex)
90+
{
91+
Log.Debug(ex, "Error wrapping Azure Event Grid events for context injection");
92+
}
93+
94+
return state;
95+
}
96+
97+
private static CallTargetState CreateProducerSpan(Tracer tracer, string? destinationName, string? host, int port, IEnumerable? events, object? singleEvent)
98+
{
99+
Scope? scope = null;
100+
101+
try
102+
{
103+
var tags = tracer.CurrentTraceSettings.Schema.Messaging.CreateAzureEventGridTags(SpanKinds.Producer);
104+
tags.MessagingOperation = "send";
105+
106+
tags.MessagingDestinationName = destinationName;
107+
tags.NetworkDestinationName = host;
108+
109+
if (port is not -1)
110+
{
111+
tags.NetworkDestinationPort = port.ToString();
112+
}
113+
114+
var messageCount = singleEvent is not null ? 1 : events is ICollection collection ? collection.Count : 0;
115+
if (messageCount > 1)
116+
{
117+
tags.MessagingBatchMessageCount = messageCount.ToString();
118+
}
119+
120+
var (serviceName, serviceNameSource) = tracer.CurrentTraceSettings.Schema.Messaging.GetServiceNameMetadata(MessagingSchema.ServiceType.AzureEventGrid);
121+
scope = tracer.StartActiveInternal("azure_eventgrid.send", tags: tags, serviceName: serviceName, serviceNameSource: serviceNameSource);
122+
var span = scope.Span;
123+
124+
span.Type = SpanTypes.Queue;
125+
span.ResourceName = destinationName;
126+
127+
if (singleEvent is not null)
128+
{
129+
ProcessEvent(singleEvent, messageCount, span, scope);
130+
}
131+
132+
tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId.AzureEventGrid);
133+
134+
return new CallTargetState(scope);
135+
}
136+
catch (Exception ex)
137+
{
138+
Log.Error(ex, "Error creating Azure Event Grid producer span");
139+
scope?.Dispose();
140+
return CallTargetState.GetDefault();
141+
}
142+
}
143+
144+
private static void ProcessEvent(object evt, int messageCount, Span span, Scope scope)
145+
{
146+
if (messageCount == 1)
147+
{
148+
SetMessageId(evt, span);
149+
}
150+
151+
// Inject W3C trace context and baggage into CloudEvent ExtensionAttributes.
152+
// CloudEvent extension attribute names only allow [a-z0-9], so we can't use
153+
// SpanContextPropagator (which also injects Datadog headers with hyphens).
154+
// Instead, inject W3C traceparent/tracestate/baggage directly — this is the standard
155+
// for CloudEvents distributed tracing. Pre-populating these keys also prevents
156+
// the Azure SDK from overwriting with its own Activity-based context.
157+
InjectContext(evt, scope);
158+
}
159+
160+
private static void InjectContext(object evt, Scope scope)
161+
{
162+
if (evt.TryDuckCast<ICloudEvent>(out var cloudEvent)
163+
&& cloudEvent.ExtensionAttributes is { } attrs)
164+
{
165+
InjectW3CContext(attrs, scope, Tracer.Instance.Settings.PropagationStyleInject);
166+
}
167+
}
168+
169+
private static void SetMessageId(object evt, Span span)
170+
{
171+
if (evt.DuckCast<IEventGridEventId>() is { Id: { } id } && id.Length > 0)
172+
{
173+
span.SetTag(Tags.MessagingMessageId, id);
174+
}
175+
}
176+
177+
/// <summary>
178+
/// Injects the configured W3C traceparent, tracestate, and baggage into CloudEvent ExtensionAttributes.
179+
/// Uses the W3C propagator directly because CloudEvent extension attribute names
180+
/// only allow lowercase letters and digits — Datadog-format headers (x-datadog-*)
181+
/// would throw ArgumentException.
182+
/// </summary>
183+
internal static void InjectW3CContext(IDictionary<string, object> extensionAttributes, Scope scope, string[] propagationStyles)
184+
{
185+
if (scope.Span.Context is not { } spanContext)
186+
{
187+
return;
188+
}
189+
190+
try
191+
{
192+
var context = new PropagationContext(spanContext, Baggage.Current);
193+
var carrier = default(Shared.AzureMessagingCommon.DictionaryContextPropagation);
194+
195+
if (IsPropagationStyleEnabled(propagationStyles, ContextPropagationHeaderStyle.W3CTraceContext) ||
196+
IsPropagationStyleEnabled(propagationStyles, ContextPropagationHeaderStyle.Deprecated.W3CTraceContext))
197+
{
198+
// The propagator does not clear an existing optional value when the current
199+
// context has none, so remove it before injecting a reused CloudEvent.
200+
extensionAttributes.Remove(W3CTraceContextPropagator.TraceStateHeaderName);
201+
W3CTraceContextPropagator.Instance.Inject(context, extensionAttributes, carrier);
202+
}
203+
204+
if (IsPropagationStyleEnabled(propagationStyles, ContextPropagationHeaderStyle.W3CBaggage))
205+
{
206+
extensionAttributes.Remove(W3CBaggagePropagator.BaggageHeaderName);
207+
W3CBaggagePropagator.Instance.Inject(context, extensionAttributes, carrier);
208+
}
209+
}
210+
catch (Exception ex)
211+
{
212+
Log.Warning(ex, "Failed to inject W3C trace context into CloudEvent ExtensionAttributes");
213+
}
214+
}
215+
216+
private static bool IsPropagationStyleEnabled(string[] propagationStyles, string expectedStyle)
217+
{
218+
foreach (var propagationStyle in propagationStyles)
219+
{
220+
if (string.Equals(propagationStyle, expectedStyle, StringComparison.OrdinalIgnoreCase))
221+
{
222+
return true;
223+
}
224+
}
225+
226+
return false;
227+
}
228+
229+
/// <summary>
230+
/// Extracts the topic name from the Event Grid endpoint host.
231+
/// The host format is "TOPIC-NAME.REGION.eventgrid.azure.net".
232+
/// </summary>
233+
private static string? GetTopicFromHost(string? host)
234+
{
235+
if (host is null)
236+
{
237+
return null;
238+
}
239+
240+
var dotIndex = host.IndexOf('.');
241+
return dotIndex > 0 ? host.Substring(0, dotIndex) : host;
242+
}
243+
244+
private static class UriBuilderFieldCache<TTarget>
245+
{
246+
public static readonly FieldInfo? Field = typeof(TTarget).GetField("_uriBuilder", BindingFlags.Instance | BindingFlags.NonPublic);
247+
}
248+
249+
private sealed class EventGridEnumerableObserver : EventGridObservingEnumerable.IObserver
250+
{
251+
private readonly Scope _scope;
252+
private readonly int? _knownCount;
253+
private readonly bool _injectContext;
254+
255+
public EventGridEnumerableObserver(Scope scope, int? knownCount, bool injectContext)
256+
{
257+
_scope = scope;
258+
_knownCount = knownCount;
259+
_injectContext = injectContext;
260+
}
261+
262+
public void OnItem(object? item)
263+
{
264+
try
265+
{
266+
if (item is not null)
267+
{
268+
if (_knownCount == 1)
269+
{
270+
SetMessageId(item, _scope.Span);
271+
}
272+
273+
if (_injectContext)
274+
{
275+
InjectContext(item, _scope);
276+
}
277+
}
278+
}
279+
catch (Exception ex)
280+
{
281+
Log.Debug(ex, "Error processing an Azure Event Grid event");
282+
}
283+
}
284+
285+
public void OnCompleted(int count, object? firstItem)
286+
{
287+
try
288+
{
289+
if (!_knownCount.HasValue && count > 1)
290+
{
291+
_scope.Span.SetTag(Tags.MessagingBatchMessageCount, count.ToString());
292+
}
293+
294+
if (!_knownCount.HasValue && count == 1 && firstItem is not null)
295+
{
296+
SetMessageId(firstItem, _scope.Span);
297+
}
298+
}
299+
catch (Exception ex)
300+
{
301+
Log.Debug(ex, "Error finalizing Azure Event Grid event processing");
302+
}
303+
}
304+
}
305+
}

0 commit comments

Comments
 (0)