Skip to content

Commit c67ae6c

Browse files
[Azure Event Grid] Add contract and core behavior
1 parent 75034ed commit c67ae6c

45 files changed

Lines changed: 2662 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: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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)
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 Azure Functions Event Grid output binding invokes EventGridPublisherClient after loading the
35+
// extension through the host's shared FunctionAssemblyLoadContext. In that scenario, accessing
36+
// _uriBuilder through a client duck-type proxy has produced MissingFieldException even though the field
37+
// exists, so retrieve it from the concrete target type before duck casting the field value.
38+
IRequestUriBuilder? uriBuilder = null;
39+
if ((object?)instance is { } target)
40+
{
41+
uriBuilder = UriBuilderFieldCache<TTarget>.Field?.GetValue(target)?.DuckCast<IRequestUriBuilder>();
42+
}
43+
44+
var host = uriBuilder?.Host;
45+
var port = uriBuilder?.Port ?? -1;
46+
return CreateProducerSpan(tracer, 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, 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, endpoint?.Host, endpoint?.Port ?? -1, ref cloudEvents, injectContext: true);
73+
}
74+
75+
private static CallTargetState CreateProducerSpan<TEvents>(Tracer tracer, string? host, int port, ref TEvents events, bool injectContext)
76+
{
77+
var enumerable = events as IEnumerable;
78+
var state = CreateProducerSpan(tracer, 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? 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.NetworkDestinationName = host;
107+
108+
if (port is not -1)
109+
{
110+
tags.NetworkDestinationPort = port.ToString();
111+
}
112+
113+
var messageCount = singleEvent is not null ? 1 : events is ICollection collection ? collection.Count : 0;
114+
if (messageCount > 1)
115+
{
116+
tags.MessagingBatchMessageCount = messageCount.ToString();
117+
}
118+
119+
var (serviceName, serviceNameSource) = tracer.CurrentTraceSettings.Schema.Messaging.GetServiceNameMetadata(MessagingSchema.ServiceType.AzureEventGrid);
120+
scope = tracer.StartActiveInternal("azure_eventgrid.send", tags: tags, serviceName: serviceName, serviceNameSource: serviceNameSource);
121+
var span = scope.Span;
122+
123+
span.Type = SpanTypes.Queue;
124+
span.ResourceName = "eventgrid";
125+
126+
if (singleEvent is not null)
127+
{
128+
ProcessEvent(singleEvent, messageCount, span, scope);
129+
}
130+
131+
tracer.TracerManager.Telemetry.IntegrationGeneratedSpan(IntegrationId.AzureEventGrid);
132+
133+
return new CallTargetState(scope);
134+
}
135+
catch (Exception ex)
136+
{
137+
Log.Error(ex, "Error creating Azure Event Grid producer span");
138+
scope?.Dispose();
139+
return CallTargetState.GetDefault();
140+
}
141+
}
142+
143+
private static void ProcessEvent(object evt, int messageCount, Span span, Scope scope)
144+
{
145+
if (messageCount == 1)
146+
{
147+
SetMessageId(evt, span);
148+
}
149+
150+
// Inject W3C trace context and baggage into CloudEvent ExtensionAttributes.
151+
// CloudEvent extension attribute names only allow [a-z0-9], so we can't use
152+
// SpanContextPropagator (which also injects Datadog headers with hyphens).
153+
// Instead, inject W3C traceparent/tracestate/baggage directly. Pre-populating these keys also prevents
154+
// the Azure SDK from overwriting with its own Activity-based context.
155+
InjectContext(evt, scope);
156+
}
157+
158+
private static void InjectContext(object evt, Scope scope)
159+
{
160+
if (evt.TryDuckCast<ICloudEvent>(out var cloudEvent)
161+
&& cloudEvent.ExtensionAttributes is { } attrs)
162+
{
163+
InjectW3CContext(attrs, scope, Tracer.Instance.Settings.PropagationStyleInject);
164+
}
165+
}
166+
167+
private static void SetMessageId(object evt, Span span)
168+
{
169+
if (evt.TryDuckCast<IEventGridEventId>(out var eventGridEvent) && eventGridEvent.Id is { Length: > 0 } id)
170+
{
171+
span.SetTag(Tags.MessagingMessageId, id);
172+
}
173+
}
174+
175+
/// <summary>
176+
/// Injects the configured W3C traceparent, tracestate, and baggage into CloudEvent ExtensionAttributes.
177+
/// Uses the W3C propagator directly because CloudEvent extension attribute names
178+
/// only allow lowercase letters and digits — Datadog-format headers (x-datadog-*)
179+
/// would throw ArgumentException.
180+
/// </summary>
181+
internal static void InjectW3CContext(IDictionary<string, object> extensionAttributes, Scope scope, string[] propagationStyles)
182+
{
183+
if (scope.Span.Context is not { } spanContext)
184+
{
185+
return;
186+
}
187+
188+
try
189+
{
190+
var context = new PropagationContext(spanContext, Baggage.Current);
191+
var carrier = default(Shared.AzureMessagingCommon.DictionaryContextPropagation);
192+
193+
if (IsPropagationStyleEnabled(propagationStyles, ContextPropagationHeaderStyle.W3CTraceContext) ||
194+
IsPropagationStyleEnabled(propagationStyles, ContextPropagationHeaderStyle.Deprecated.W3CTraceContext))
195+
{
196+
W3CTraceContextPropagator.Instance.Inject(context, extensionAttributes, carrier);
197+
}
198+
199+
if (IsPropagationStyleEnabled(propagationStyles, ContextPropagationHeaderStyle.W3CBaggage))
200+
{
201+
W3CBaggagePropagator.Instance.Inject(context, extensionAttributes, carrier);
202+
}
203+
}
204+
catch (Exception ex)
205+
{
206+
Log.Warning(ex, "Failed to inject W3C trace context into CloudEvent ExtensionAttributes");
207+
}
208+
}
209+
210+
private static bool IsPropagationStyleEnabled(string[] propagationStyles, string expectedStyle)
211+
{
212+
foreach (var propagationStyle in propagationStyles)
213+
{
214+
if (string.Equals(propagationStyle, expectedStyle, StringComparison.OrdinalIgnoreCase))
215+
{
216+
return true;
217+
}
218+
}
219+
220+
return false;
221+
}
222+
223+
private static class UriBuilderFieldCache<TTarget>
224+
{
225+
public static readonly FieldInfo? Field = typeof(TTarget).GetField("_uriBuilder", BindingFlags.Instance | BindingFlags.NonPublic);
226+
}
227+
228+
private sealed class EventGridEnumerableObserver : EventGridObservingEnumerable.IObserver
229+
{
230+
private readonly Scope _scope;
231+
private readonly int? _knownCount;
232+
private readonly bool _injectContext;
233+
234+
public EventGridEnumerableObserver(Scope scope, int? knownCount, bool injectContext)
235+
{
236+
_scope = scope;
237+
_knownCount = knownCount;
238+
_injectContext = injectContext;
239+
}
240+
241+
public void OnItem(object? item)
242+
{
243+
try
244+
{
245+
if (item is not null)
246+
{
247+
if (_knownCount == 1)
248+
{
249+
SetMessageId(item, _scope.Span);
250+
}
251+
252+
if (_injectContext)
253+
{
254+
InjectContext(item, _scope);
255+
}
256+
}
257+
}
258+
catch (Exception ex)
259+
{
260+
Log.Debug(ex, "Error processing an Azure Event Grid event");
261+
}
262+
}
263+
264+
public void OnEnumerationCompleted(int count, object? firstItem)
265+
{
266+
try
267+
{
268+
if (!_knownCount.HasValue && count > 1)
269+
{
270+
_scope.Span.SetTag(Tags.MessagingBatchMessageCount, count.ToString());
271+
}
272+
273+
if (!_knownCount.HasValue && count == 1 && firstItem is not null)
274+
{
275+
SetMessageId(firstItem, _scope.Span);
276+
}
277+
}
278+
catch (Exception ex)
279+
{
280+
Log.Debug(ex, "Error finalizing Azure Event Grid event processing");
281+
}
282+
}
283+
}
284+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// <copyright file="EventGridObservingEnumerable.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.Generic;
10+
11+
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Azure.EventGrid;
12+
13+
/// <summary>
14+
/// Replaces a declared <see cref="IEnumerable{T}"/> argument with an observing wrapper without
15+
/// taking a compile-time dependency on the enumerable's item type.
16+
/// </summary>
17+
/// <remarks>
18+
/// Event Grid send APIs accept deferred or one-shot sequences. Enumerating them in the CallTarget begin callback
19+
/// could execute customer code early, consume the sequence, or observe different event instances from those the
20+
/// Azure SDK eventually sends. This wrapper observes items only as the SDK enumerates them, preserving the source's
21+
/// enumeration timing and item flow. It is constructed dynamically because the tracer cannot reference the Azure SDK
22+
/// event types.
23+
/// </remarks>
24+
internal static class EventGridObservingEnumerable
25+
{
26+
internal interface IObserver
27+
{
28+
void OnItem(object? item);
29+
30+
void OnEnumerationCompleted(int count, object? firstItem);
31+
}
32+
33+
private interface IEnumerableWrapperFactory
34+
{
35+
object Create(object events, IObserver observer);
36+
}
37+
38+
internal static TEvents Wrap<TEvents>(TEvents events, IObserver observer)
39+
{
40+
if ((object?)events is null || FactoryCache<TEvents>.Factory is not { } factory)
41+
{
42+
return events;
43+
}
44+
45+
return (TEvents)factory.Create(events, observer);
46+
}
47+
48+
private static IEnumerable<TEvent> Observe<TEvent>(IEnumerable<TEvent> events, IObserver observer)
49+
{
50+
object? firstItem = null;
51+
var count = 0;
52+
53+
foreach (var item in events)
54+
{
55+
if (count == 0)
56+
{
57+
firstItem = item;
58+
}
59+
60+
count++;
61+
try
62+
{
63+
observer.OnItem(item);
64+
}
65+
catch
66+
{
67+
// Instrumentation must not affect customer enumeration.
68+
}
69+
70+
yield return item;
71+
}
72+
73+
try
74+
{
75+
observer.OnEnumerationCompleted(count, firstItem);
76+
}
77+
catch
78+
{
79+
// Instrumentation must not affect customer enumeration.
80+
}
81+
}
82+
83+
// CallTarget supplies TEvents as the method's declared parameter type, for example IEnumerable<CloudEvent>.
84+
// The tracer cannot reference CloudEvent directly, so reflection closes EnumerableWrapperFactory<TEvent> once.
85+
// The resulting factory is cached, and subsequent calls perform no reflection.
86+
private static class FactoryCache<TEvents>
87+
{
88+
public static readonly IEnumerableWrapperFactory? Factory = CreateFactory();
89+
90+
private static IEnumerableWrapperFactory? CreateFactory()
91+
{
92+
var eventsType = typeof(TEvents);
93+
if (!eventsType.IsGenericType || eventsType.GetGenericTypeDefinition() != typeof(IEnumerable<>))
94+
{
95+
return null;
96+
}
97+
98+
var eventType = eventsType.GetGenericArguments()[0];
99+
var factoryType = typeof(EnumerableWrapperFactory<>).MakeGenericType(eventType);
100+
return Activator.CreateInstance(factoryType, nonPublic: true) as IEnumerableWrapperFactory;
101+
}
102+
}
103+
104+
private sealed class EnumerableWrapperFactory<TEvent> : IEnumerableWrapperFactory
105+
{
106+
public object Create(object events, IObserver observer)
107+
=> Observe((IEnumerable<TEvent>)events, observer);
108+
}
109+
}

0 commit comments

Comments
 (0)