-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Add EventGrid distributed tracing #15850
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
6c08b4c
c7749ba
66070e9
e8f0686
6cf5d79
823f9e2
fb6b320
0b52b7b
bb47152
42be188
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,13 +9,18 @@ | |
| import com.azure.core.http.HttpPipeline; | ||
| import com.azure.core.http.rest.Response; | ||
| import com.azure.core.util.Context; | ||
| import com.azure.core.util.logging.ClientLogger; | ||
| import com.azure.core.util.serializer.SerializerAdapter; | ||
| import com.azure.core.util.tracing.TracerProxy; | ||
| import com.azure.messaging.eventgrid.implementation.Constants; | ||
| import com.azure.messaging.eventgrid.implementation.EventGridPublisherClientImpl; | ||
| import com.azure.messaging.eventgrid.implementation.EventGridPublisherClientImplBuilder; | ||
| import reactor.core.publisher.Flux; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| import static com.azure.core.util.FluxUtil.monoError; | ||
| import static com.azure.core.util.FluxUtil.withContext; | ||
| import static com.azure.core.util.tracing.Tracer.AZ_TRACING_NAMESPACE_KEY; | ||
|
|
||
| /** | ||
| * A service client that publishes events to an EventGrid topic or domain. Use {@link EventGridPublisherClientBuilder} | ||
|
|
@@ -33,6 +38,9 @@ public final class EventGridPublisherAsyncClient { | |
|
|
||
| private final EventGridServiceVersion serviceVersion; | ||
|
|
||
| private final ClientLogger logger = new ClientLogger(EventGridPublisherAsyncClient.class); | ||
|
|
||
|
|
||
| EventGridPublisherAsyncClient(HttpPipeline pipeline, String hostname, SerializerAdapter serializerAdapter, | ||
| EventGridServiceVersion serviceVersion) { | ||
| this.impl = new EventGridPublisherClientImplBuilder() | ||
|
|
@@ -63,14 +71,18 @@ public EventGridServiceVersion getServiceVersion() { | |
| */ | ||
| @ServiceMethod(returns = ReturnType.SINGLE) | ||
| public Mono<Void> sendEvents(Iterable<EventGridEvent> events) { | ||
| if (events == null) { | ||
| return monoError(logger, new NullPointerException("'events' cannot be null.")); | ||
| } | ||
| return withContext(context -> sendEvents(events, context)); | ||
| } | ||
|
|
||
| Mono<Void> sendEvents(Iterable<EventGridEvent> events, Context context) { | ||
| return Flux.fromIterable(events) | ||
| .map(EventGridEvent::toImpl) | ||
| .collectList() | ||
| .flatMap(list -> this.impl.publishEventsAsync(this.hostname, list, context)); | ||
| .flatMap(list -> this.impl.publishEventsAsync(this.hostname, | ||
| list, context.addData(AZ_TRACING_NAMESPACE_KEY, Constants.EVENT_GRID_TRACING_NAMESPACE_VALUE))); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -81,14 +93,19 @@ Mono<Void> sendEvents(Iterable<EventGridEvent> events, Context context) { | |
| */ | ||
| @ServiceMethod(returns = ReturnType.SINGLE) | ||
| public Mono<Void> sendCloudEvents(Iterable<CloudEvent> events) { | ||
| if (events == null) { | ||
| return monoError(logger, new NullPointerException("'events' cannot be null.")); | ||
| } | ||
| return withContext(context -> sendCloudEvents(events, context)); | ||
| } | ||
|
|
||
| Mono<Void> sendCloudEvents(Iterable<CloudEvent> events, Context context) { | ||
| this.addCloudEventTracePlaceHolder(events); | ||
| return Flux.fromIterable(events) | ||
| .map(CloudEvent::toImpl) | ||
| .collectList() | ||
| .flatMap(list -> this.impl.publishCloudEventEventsAsync(this.hostname, list, context)); | ||
| .flatMap(list -> this.impl.publishCloudEventEventsAsync(this.hostname, list, | ||
| context.addData(AZ_TRACING_NAMESPACE_KEY, Constants.EVENT_GRID_TRACING_NAMESPACE_VALUE))); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sure to check context for null on the calling function to avoid NPE when doing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In some cases, we end up calling this from the sync client and hence the |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -99,13 +116,17 @@ Mono<Void> sendCloudEvents(Iterable<CloudEvent> events, Context context) { | |
| */ | ||
| @ServiceMethod(returns = ReturnType.SINGLE) | ||
| public Mono<Void> sendCustomEvents(Iterable<Object> events) { | ||
| if (events == null) { | ||
| return monoError(logger, new NullPointerException("'events' cannot be null.")); | ||
| } | ||
| return withContext(context -> sendCustomEvents(events, context)); | ||
| } | ||
|
|
||
| Mono<Void> sendCustomEvents(Iterable<Object> events, Context context) { | ||
| return Flux.fromIterable(events) | ||
| .collectList() | ||
| .flatMap(list -> this.impl.publishCustomEventEventsAsync(this.hostname, list, context)); | ||
| .flatMap(list -> this.impl.publishCustomEventEventsAsync(this.hostname, list, | ||
| context.addData(AZ_TRACING_NAMESPACE_KEY, Constants.EVENT_GRID_TRACING_NAMESPACE_VALUE))); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -116,14 +137,18 @@ Mono<Void> sendCustomEvents(Iterable<Object> events, Context context) { | |
| */ | ||
| @ServiceMethod(returns = ReturnType.SINGLE) | ||
| public Mono<Response<Void>> sendEventsWithResponse(Iterable<EventGridEvent> events) { | ||
| if (events == null) { | ||
| return monoError(logger, new NullPointerException("'events' cannot be null.")); | ||
| } | ||
| return withContext(context -> sendEventsWithResponse(events, context)); | ||
| } | ||
|
|
||
| Mono<Response<Void>> sendEventsWithResponse(Iterable<EventGridEvent> events, Context context) { | ||
| return Flux.fromIterable(events) | ||
| .map(EventGridEvent::toImpl) | ||
| .collectList() | ||
| .flatMap(list -> this.impl.publishEventsWithResponseAsync(this.hostname, list, context)); | ||
| .flatMap(list -> this.impl.publishEventsWithResponseAsync(this.hostname, list, | ||
| context.addData(AZ_TRACING_NAMESPACE_KEY, Constants.EVENT_GRID_TRACING_NAMESPACE_VALUE))); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think my comment about user passing null for context applies to this method. |
||
| } | ||
|
|
||
| /** | ||
|
|
@@ -134,14 +159,19 @@ Mono<Response<Void>> sendEventsWithResponse(Iterable<EventGridEvent> events, Con | |
| */ | ||
| @ServiceMethod(returns = ReturnType.SINGLE) | ||
| public Mono<Response<Void>> sendCloudEventsWithResponse(Iterable<CloudEvent> events) { | ||
| if (events == null) { | ||
| return monoError(logger, new NullPointerException("'events' cannot be null.")); | ||
| } | ||
| return withContext(context -> sendCloudEventsWithResponse(events, context)); | ||
| } | ||
|
|
||
| Mono<Response<Void>> sendCloudEventsWithResponse(Iterable<CloudEvent> events, Context context) { | ||
| this.addCloudEventTracePlaceHolder(events); | ||
| return Flux.fromIterable(events) | ||
| .map(CloudEvent::toImpl) | ||
| .collectList() | ||
| .flatMap(list -> this.impl.publishCloudEventEventsWithResponseAsync(this.hostname, list, context)); | ||
| .flatMap(list -> this.impl.publishCloudEventEventsWithResponseAsync(this.hostname, list, | ||
| context.addData(AZ_TRACING_NAMESPACE_KEY, Constants.EVENT_GRID_TRACING_NAMESPACE_VALUE))); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -152,12 +182,30 @@ Mono<Response<Void>> sendCloudEventsWithResponse(Iterable<CloudEvent> events, Co | |
| */ | ||
| @ServiceMethod(returns = ReturnType.SINGLE) | ||
| public Mono<Response<Void>> sendCustomEventsWithResponse(Iterable<Object> events) { | ||
| if (events == null) { | ||
| return monoError(logger, new NullPointerException("'events' cannot be null.")); | ||
| } | ||
| return withContext(context -> sendCustomEventsWithResponse(events, context)); | ||
| } | ||
|
|
||
| Mono<Response<Void>> sendCustomEventsWithResponse(Iterable<Object> events, Context context) { | ||
| return Flux.fromIterable(events) | ||
| .collectList() | ||
| .flatMap(list -> this.impl.publishCustomEventEventsWithResponseAsync(this.hostname, list, context)); | ||
| .flatMap(list -> this.impl.publishCustomEventEventsWithResponseAsync(this.hostname, list, | ||
| context.addData(AZ_TRACING_NAMESPACE_KEY, Constants.EVENT_GRID_TRACING_NAMESPACE_VALUE))); | ||
| } | ||
|
|
||
| private void addCloudEventTracePlaceHolder(Iterable<CloudEvent> events) { | ||
| if (TracerProxy.isTracingEnabled()) { | ||
| for (CloudEvent event : events) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added null check like in EventHubs |
||
| if (event.getExtensionAttributes() == null || | ||
| (event.getExtensionAttributes().get(Constants.TRACE_PARENT) == null && | ||
| event.getExtensionAttributes().get(Constants.TRACE_STATE) == null)) { | ||
|
|
||
| event.addExtensionAttribute(Constants.TRACE_PARENT, Constants.TRACE_PARENT_PLACEHOLDER_UUID); | ||
| event.addExtensionAttribute(Constants.TRACE_STATE, Constants.TRACE_STATE_PLACEHOLDER_UUID); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| package com.azure.messaging.eventgrid.implementation; | ||
|
|
||
| import com.azure.core.http.HttpHeader; | ||
| import com.azure.core.http.HttpPipelineCallContext; | ||
| import com.azure.core.http.HttpPipelineNextPolicy; | ||
| import com.azure.core.http.HttpRequest; | ||
| import com.azure.core.http.HttpResponse; | ||
| import com.azure.core.http.policy.HttpPipelinePolicy; | ||
| import com.azure.core.util.tracing.TracerProxy; | ||
| import reactor.core.publisher.Mono; | ||
|
|
||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| import com.azure.messaging.eventgrid.CloudEvent; | ||
| /** | ||
| * This pipeline policy should be added after OpenTelemetryPolicy in the http pipeline. | ||
| * | ||
| * It checks whether the {@link HttpRequest} headers have "traceparent" or "tracestate" and whether the serialized | ||
| * http body json string for a list of {@link CloudEvent} instances has place holders | ||
| * {@link Constants#TRACE_PARENT_PLACEHOLDER} or {@link Constants#TRACE_STATE_PLACEHOLDER}. | ||
| * The place holders will be replaced by the value from headers if the headers have "traceparent" or "tracestate", | ||
| * or be removed if the headers don't have. | ||
| * | ||
| * The place holders won't exist in the json string if the {@link TracerProxy#isTracingEnabled()} returns false. | ||
| */ | ||
| public class CloudEventTracingPipelinePolicy implements HttpPipelinePolicy { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add javadoc
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
| @Override | ||
| public Mono<HttpResponse> process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { | ||
| final HttpRequest request = context.getHttpRequest(); | ||
| final HttpHeader contentType = request.getHeaders().get(Constants.CONTENT_TYPE); | ||
| StringBuilder bodyStringBuilder = new StringBuilder(); | ||
| if (TracerProxy.isTracingEnabled() && contentType != null && | ||
| Constants.CLOUD_EVENT_CONTENT_TYPE.equals(contentType.getValue())) { | ||
| return request.getBody().map(byteBuffer -> bodyStringBuilder.append(new String(byteBuffer.array(), | ||
| StandardCharsets.UTF_8))) | ||
| .then(Mono.fromCallable(() -> replaceTracingPlaceHolder(request, bodyStringBuilder))) | ||
| .then(next.process()); | ||
| } | ||
| else { | ||
| return next.process(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * | ||
| * @param request The {@link HttpRequest}, whose body will be mutated by replacing traceparent and tracestate | ||
| * placeholders. | ||
| * @param bodyStringBuilder The {@link StringBuilder} that contains the full HttpRequest body string. | ||
| * @return The new body string with the place holders replaced (if header has tracing) | ||
| * or removed (if header no tracing). | ||
| */ | ||
| static String replaceTracingPlaceHolder(HttpRequest request, StringBuilder bodyStringBuilder) { | ||
| final int traceParentPlaceHolderIndex = bodyStringBuilder.indexOf(Constants.TRACE_PARENT_PLACEHOLDER); | ||
| if (traceParentPlaceHolderIndex >= 0) { // There is "traceparent" placeholder in body, replace it. | ||
| final HttpHeader traceparentHeader = request.getHeaders().get(Constants.TRACE_PARENT); | ||
| bodyStringBuilder.replace(traceParentPlaceHolderIndex, | ||
| Constants.TRACE_PARENT_PLACEHOLDER.length() + traceParentPlaceHolderIndex, | ||
| traceparentHeader != null | ||
| ? String.format(",\"%s\":\"%s\"", Constants.TRACE_PARENT, traceparentHeader.getValue()) | ||
| : ""); | ||
|
samvaity marked this conversation as resolved.
|
||
| } | ||
| final int traceStatePlaceHolderIndex = bodyStringBuilder.indexOf(Constants.TRACE_STATE_PLACEHOLDER); | ||
| if (traceStatePlaceHolderIndex >= 0) { // There is "tracestate" placeholder in body, replace it. | ||
| final HttpHeader tracestateHeader = request.getHeaders().get(Constants.TRACE_STATE); | ||
| bodyStringBuilder.replace(traceStatePlaceHolderIndex, | ||
| Constants.TRACE_STATE_PLACEHOLDER.length() + traceStatePlaceHolderIndex, | ||
| tracestateHeader != null | ||
| ? String.format(",\"%s\":\"%s\"", Constants.TRACE_STATE, tracestateHeader.getValue()) | ||
| : ""); | ||
| } | ||
| String newBodyString = bodyStringBuilder.toString(); | ||
| request.setHeader(Constants.CONTENT_LENGTH, String.valueOf(newBodyString.length())); | ||
| request.setBody(newBodyString); | ||
| return newBodyString; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.azure.messaging.eventgrid.implementation; | ||
|
|
||
| public class Constants { | ||
| public static final String CONTENT_TYPE = "Content-Type"; | ||
| public static final String CONTENT_LENGTH = "Content-Length"; | ||
| public static final String CLOUD_EVENT_CONTENT_TYPE = "application/cloudevents-batch+json; charset=utf-8"; | ||
| public static final String TRACE_PARENT = "traceparent"; | ||
| public static final String TRACE_STATE = "tracestate"; | ||
| public static final String TRACE_PARENT_PLACEHOLDER_UUID = "TP-14b6b15b-74b6-4178-847e-d142aa2727b2"; | ||
| public static final String TRACE_STATE_PLACEHOLDER_UUID = "TS-14b6b15b-74b6-4178-847e-d142aa2727b2"; | ||
| public static final String TRACE_PARENT_PLACEHOLDER = ",\"" + TRACE_PARENT + "\":\"TP-14b6b15b-74b6-4178-847e-d142aa2727b2\""; | ||
| public static final String TRACE_STATE_PLACEHOLDER = ",\"" + TRACE_STATE + "\":\"TS-14b6b15b-74b6-4178-847e-d142aa2727b2\""; | ||
|
|
||
| // Please see <a href=https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-services-resource-providers>here</a> | ||
| // for more information on Azure resource provider namespaces. | ||
| public static final String EVENT_GRID_TRACING_NAMESPACE_VALUE = "Microsoft.EventGrid"; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should have a null check for context before using it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This internal api is called by a public API, which calls
FluxUtil.withContextto create a Context. So I assume the context won't be null.In debugging, I see it's an empty Context instance instead of null.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@YijunXieMS - this method is also called from sync client and the user can pass a null context.
User can call
sendEventsWithResponse(events, null)