From 485409ec18cdab482482fd532b28b5a304c6de20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 10 Aug 2022 19:37:25 +0200 Subject: [PATCH] [Instrumenation.GrpcCore] File scoped namespace (#575) --- .../AsyncStreamReaderProxy.cs | 127 ++- .../ClientStreamWriterProxy.cs | 145 ++-- .../ClientTracingInterceptor.cs | 605 +++++++------ .../ClientTracingInterceptorOptions.cs | 39 +- .../Extensions.cs | 45 +- .../GrpcCoreInstrumentation.cs | 49 +- .../RpcScope.cs | 347 ++++---- .../SemanticConventions.cs | 37 +- .../ServerStreamWriterProxy.cs | 87 +- .../ServerTracingInterceptor.cs | 335 ++++--- .../ServerTracingInterceptorOptions.cs | 39 +- .../TracerProviderBuilderExtensions.cs | 33 +- .../FoobarService.cs | 409 +++++---- .../GrpcCoreClientInterceptorTests.cs | 821 +++++++++--------- .../GrpcCoreServerInterceptorTests.cs | 255 +++--- .../InterceptorActivityListener.cs | 83 +- 16 files changed, 1720 insertions(+), 1736 deletions(-) diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/AsyncStreamReaderProxy.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/AsyncStreamReaderProxy.cs index fdeb782a550..cc4b603d14f 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/AsyncStreamReaderProxy.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/AsyncStreamReaderProxy.cs @@ -14,84 +14,83 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using global::Grpc.Core; +using System; +using System.Threading; +using System.Threading.Tasks; +using global::Grpc.Core; + +namespace OpenTelemetry.Instrumentation.GrpcCore; +/// +/// A proxy stream reader with callbacks for interesting events. +/// +/// +/// Borrowed heavily from +/// https://github.com/opentracing-contrib/csharp-grpc/blob/master/src/OpenTracing.Contrib.Grpc/Streaming/TracingAsyncStreamReader.cs. +/// +/// The message type. +/// +internal class AsyncStreamReaderProxy : IAsyncStreamReader +{ /// - /// A proxy stream reader with callbacks for interesting events. + /// The reader. /// - /// - /// Borrowed heavily from - /// https://github.com/opentracing-contrib/csharp-grpc/blob/master/src/OpenTracing.Contrib.Grpc/Streaming/TracingAsyncStreamReader.cs. - /// - /// The message type. - /// - internal class AsyncStreamReaderProxy : IAsyncStreamReader - { - /// - /// The reader. - /// - private readonly IAsyncStreamReader reader; + private readonly IAsyncStreamReader reader; - /// - /// The on message action. - /// - private readonly Action onMessage; + /// + /// The on message action. + /// + private readonly Action onMessage; - /// - /// The on stream end action. - /// - private readonly Action onStreamEnd; + /// + /// The on stream end action. + /// + private readonly Action onStreamEnd; - /// - /// The on exception action. - /// - private readonly Action onException; + /// + /// The on exception action. + /// + private readonly Action onException; - /// - /// Initializes a new instance of the class. - /// - /// The reader. - /// The on message action, if any. - /// The on stream end action, if any. - /// The on exception action, if any. - public AsyncStreamReaderProxy(IAsyncStreamReader reader, Action onMessage = null, Action onStreamEnd = null, Action onException = null) - { - this.reader = reader; - this.onMessage = onMessage; - this.onStreamEnd = onStreamEnd; - this.onException = onException; - } + /// + /// Initializes a new instance of the class. + /// + /// The reader. + /// The on message action, if any. + /// The on stream end action, if any. + /// The on exception action, if any. + public AsyncStreamReaderProxy(IAsyncStreamReader reader, Action onMessage = null, Action onStreamEnd = null, Action onException = null) + { + this.reader = reader; + this.onMessage = onMessage; + this.onStreamEnd = onStreamEnd; + this.onException = onException; + } - /// - public T Current => this.reader.Current; + /// + public T Current => this.reader.Current; - /// - public async Task MoveNext(CancellationToken cancellationToken) + /// + public async Task MoveNext(CancellationToken cancellationToken) + { + try { - try + var hasNext = await this.reader.MoveNext(cancellationToken).ConfigureAwait(false); + if (hasNext) { - var hasNext = await this.reader.MoveNext(cancellationToken).ConfigureAwait(false); - if (hasNext) - { - this.onMessage?.Invoke(this.Current); - } - else - { - this.onStreamEnd?.Invoke(); - } - - return hasNext; + this.onMessage?.Invoke(this.Current); } - catch (Exception ex) + else { - this.onException?.Invoke(ex); - throw; + this.onStreamEnd?.Invoke(); } + + return hasNext; + } + catch (Exception ex) + { + this.onException?.Invoke(ex); + throw; } } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/ClientStreamWriterProxy.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/ClientStreamWriterProxy.cs index ae0e3b05bb5..95fa3f69fb9 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/ClientStreamWriterProxy.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/ClientStreamWriterProxy.cs @@ -14,95 +14,94 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore +using System; +using System.Threading.Tasks; +using global::Grpc.Core; + +namespace OpenTelemetry.Instrumentation.GrpcCore; + +/// +/// A proxy client stream writer. +/// +/// +/// Borrowed heavily from +/// https://github.com/opentracing-contrib/csharp-grpc/blob/master/src/OpenTracing.Contrib.Grpc/Streaming/TracingClientStreamWriter.cs. +/// +/// The message type. +/// +internal class ClientStreamWriterProxy : IClientStreamWriter { - using System; - using System.Threading.Tasks; - using global::Grpc.Core; + /// + /// The writer. + /// + private readonly IClientStreamWriter writer; /// - /// A proxy client stream writer. + /// The on write action. /// - /// - /// Borrowed heavily from - /// https://github.com/opentracing-contrib/csharp-grpc/blob/master/src/OpenTracing.Contrib.Grpc/Streaming/TracingClientStreamWriter.cs. - /// - /// The message type. - /// - internal class ClientStreamWriterProxy : IClientStreamWriter - { - /// - /// The writer. - /// - private readonly IClientStreamWriter writer; + private readonly Action onWrite; - /// - /// The on write action. - /// - private readonly Action onWrite; + /// + /// The on complete action. + /// + private readonly Action onComplete; - /// - /// The on complete action. - /// - private readonly Action onComplete; + /// + /// The on exception action. + /// + private readonly Action onException; - /// - /// The on exception action. - /// - private readonly Action onException; + /// + /// Initializes a new instance of the class. + /// + /// The writer. + /// The on write action if any. + /// The on complete action, if any. + /// The on exception action, if any. + public ClientStreamWriterProxy(IClientStreamWriter writer, Action onWrite = null, Action onComplete = null, Action onException = null) + { + this.writer = writer; + this.onWrite = onWrite; + this.onComplete = onComplete; + this.onException = onException; + } - /// - /// Initializes a new instance of the class. - /// - /// The writer. - /// The on write action if any. - /// The on complete action, if any. - /// The on exception action, if any. - public ClientStreamWriterProxy(IClientStreamWriter writer, Action onWrite = null, Action onComplete = null, Action onException = null) + /// + public WriteOptions WriteOptions + { + get => this.writer.WriteOptions; + set => this.writer.WriteOptions = value; + } + + /// + public async Task WriteAsync(T message) + { + this.onWrite?.Invoke(message); + + try { - this.writer = writer; - this.onWrite = onWrite; - this.onComplete = onComplete; - this.onException = onException; + await this.writer.WriteAsync(message).ConfigureAwait(false); } - - /// - public WriteOptions WriteOptions + catch (Exception e) { - get => this.writer.WriteOptions; - set => this.writer.WriteOptions = value; + this.onException?.Invoke(e); + throw; } + } - /// - public async Task WriteAsync(T message) - { - this.onWrite?.Invoke(message); + /// + public async Task CompleteAsync() + { + this.onComplete?.Invoke(); - try - { - await this.writer.WriteAsync(message).ConfigureAwait(false); - } - catch (Exception e) - { - this.onException?.Invoke(e); - throw; - } + try + { + await this.writer.CompleteAsync().ConfigureAwait(false); } - - /// - public async Task CompleteAsync() + catch (Exception e) { - this.onComplete?.Invoke(); - - try - { - await this.writer.CompleteAsync().ConfigureAwait(false); - } - catch (Exception e) - { - this.onException?.Invoke(e); - throw; - } + this.onException?.Invoke(e); + throw; } } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptor.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptor.cs index 5bef4540bab..72716be9510 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptor.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptor.cs @@ -14,350 +14,349 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore +using System; +using System.Collections.Generic; +using System.Diagnostics; +using global::Grpc.Core; +using global::Grpc.Core.Interceptors; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.GrpcCore; + +/// +/// A client interceptor that starts and stops an Activity for each outbound RPC. +/// +/// +public class ClientTracingInterceptor : Interceptor { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using global::Grpc.Core; - using global::Grpc.Core.Interceptors; - using OpenTelemetry.Context.Propagation; - using OpenTelemetry.Internal; + /// + /// The options. + /// + private readonly ClientTracingInterceptorOptions options; /// - /// A client interceptor that starts and stops an Activity for each outbound RPC. + /// Initializes a new instance of the class. /// - /// - public class ClientTracingInterceptor : Interceptor + /// The options. + public ClientTracingInterceptor(ClientTracingInterceptorOptions options) { - /// - /// The options. - /// - private readonly ClientTracingInterceptorOptions options; + Guard.ThrowIfNull(options); - /// - /// Initializes a new instance of the class. - /// - /// The options. - public ClientTracingInterceptor(ClientTracingInterceptorOptions options) - { - Guard.ThrowIfNull(options); + this.options = options; + } - this.options = options; - } + /// + public override TResponse BlockingUnaryCall( + TRequest request, + ClientInterceptorContext context, + BlockingUnaryCallContinuation continuation) + { + ClientRpcScope rpcScope = null; - /// - public override TResponse BlockingUnaryCall( - TRequest request, - ClientInterceptorContext context, - BlockingUnaryCallContinuation continuation) + try { - ClientRpcScope rpcScope = null; - - try - { - rpcScope = new ClientRpcScope(context, this.options); - rpcScope.RecordRequest(request); - var response = continuation(request, rpcScope.Context); - rpcScope.RecordResponse(response); - rpcScope.Complete(); - return response; - } - catch (Exception e) - { - rpcScope?.CompleteWithException(e); - throw; - } - finally - { - rpcScope?.RestoreParentActivity(); - rpcScope?.Dispose(); - } + rpcScope = new ClientRpcScope(context, this.options); + rpcScope.RecordRequest(request); + var response = continuation(request, rpcScope.Context); + rpcScope.RecordResponse(response); + rpcScope.Complete(); + return response; } - - /// - public override AsyncUnaryCall AsyncUnaryCall( - TRequest request, - ClientInterceptorContext context, - AsyncUnaryCallContinuation continuation) + catch (Exception e) + { + rpcScope?.CompleteWithException(e); + throw; + } + finally { - ClientRpcScope rpcScope = null; + rpcScope?.RestoreParentActivity(); + rpcScope?.Dispose(); + } + } - try - { - rpcScope = new ClientRpcScope(context, this.options); - rpcScope.RecordRequest(request); - var responseContinuation = continuation(request, rpcScope.Context); - var responseAsync = responseContinuation.ResponseAsync.ContinueWith( - responseTask => + /// + public override AsyncUnaryCall AsyncUnaryCall( + TRequest request, + ClientInterceptorContext context, + AsyncUnaryCallContinuation continuation) + { + ClientRpcScope rpcScope = null; + + try + { + rpcScope = new ClientRpcScope(context, this.options); + rpcScope.RecordRequest(request); + var responseContinuation = continuation(request, rpcScope.Context); + var responseAsync = responseContinuation.ResponseAsync.ContinueWith( + responseTask => + { + try { - try - { - var response = responseTask.Result; - rpcScope.RecordResponse(response); - rpcScope.Complete(); - return response; - } - catch (AggregateException ex) - { - rpcScope.CompleteWithException(ex.InnerException); - throw ex.InnerException; - } - }); - - return new AsyncUnaryCall( - responseAsync, - responseContinuation.ResponseHeadersAsync, - responseContinuation.GetStatus, - responseContinuation.GetTrailers, - responseContinuation.WithBestEffortDispose(rpcScope)); - } - catch (Exception e) - { - rpcScope?.CompleteWithException(e); - throw; - } - finally - { - rpcScope?.RestoreParentActivity(); - } + var response = responseTask.Result; + rpcScope.RecordResponse(response); + rpcScope.Complete(); + return response; + } + catch (AggregateException ex) + { + rpcScope.CompleteWithException(ex.InnerException); + throw ex.InnerException; + } + }); + + return new AsyncUnaryCall( + responseAsync, + responseContinuation.ResponseHeadersAsync, + responseContinuation.GetStatus, + responseContinuation.GetTrailers, + responseContinuation.WithBestEffortDispose(rpcScope)); } - - /// - public override AsyncClientStreamingCall AsyncClientStreamingCall( - ClientInterceptorContext context, - AsyncClientStreamingCallContinuation continuation) + catch (Exception e) + { + rpcScope?.CompleteWithException(e); + throw; + } + finally { - ClientRpcScope rpcScope = null; + rpcScope?.RestoreParentActivity(); + } + } - try - { - rpcScope = new ClientRpcScope(context, this.options); - var responseContinuation = continuation(rpcScope.Context); - var clientRequestStreamProxy = new ClientStreamWriterProxy( - responseContinuation.RequestStream, - rpcScope.RecordRequest, - onException: rpcScope.CompleteWithException); - - var responseAsync = responseContinuation.ResponseAsync.ContinueWith( - responseTask => + /// + public override AsyncClientStreamingCall AsyncClientStreamingCall( + ClientInterceptorContext context, + AsyncClientStreamingCallContinuation continuation) + { + ClientRpcScope rpcScope = null; + + try + { + rpcScope = new ClientRpcScope(context, this.options); + var responseContinuation = continuation(rpcScope.Context); + var clientRequestStreamProxy = new ClientStreamWriterProxy( + responseContinuation.RequestStream, + rpcScope.RecordRequest, + onException: rpcScope.CompleteWithException); + + var responseAsync = responseContinuation.ResponseAsync.ContinueWith( + responseTask => + { + try { - try - { - var response = responseTask.Result; - rpcScope.RecordResponse(response); - rpcScope.Complete(); - return response; - } - catch (AggregateException ex) - { - rpcScope.CompleteWithException(ex.InnerException); - throw ex.InnerException; - } - }); - - return new AsyncClientStreamingCall( - clientRequestStreamProxy, - responseAsync, - responseContinuation.ResponseHeadersAsync, - responseContinuation.GetStatus, - responseContinuation.GetTrailers, - responseContinuation.WithBestEffortDispose(rpcScope)); - } - catch (Exception e) - { - rpcScope?.CompleteWithException(e); - throw; - } - finally - { - rpcScope?.RestoreParentActivity(); - } + var response = responseTask.Result; + rpcScope.RecordResponse(response); + rpcScope.Complete(); + return response; + } + catch (AggregateException ex) + { + rpcScope.CompleteWithException(ex.InnerException); + throw ex.InnerException; + } + }); + + return new AsyncClientStreamingCall( + clientRequestStreamProxy, + responseAsync, + responseContinuation.ResponseHeadersAsync, + responseContinuation.GetStatus, + responseContinuation.GetTrailers, + responseContinuation.WithBestEffortDispose(rpcScope)); } - - /// - public override AsyncServerStreamingCall AsyncServerStreamingCall( - TRequest request, - ClientInterceptorContext context, - AsyncServerStreamingCallContinuation continuation) + catch (Exception e) { - ClientRpcScope rpcScope = null; - - try - { - rpcScope = new ClientRpcScope(context, this.options); - rpcScope.RecordRequest(request); - var responseContinuation = continuation(request, rpcScope.Context); - - var responseStreamProxy = new AsyncStreamReaderProxy( - responseContinuation.ResponseStream, - rpcScope.RecordResponse, - rpcScope.Complete, - rpcScope.CompleteWithException); - - return new AsyncServerStreamingCall( - responseStreamProxy, - responseContinuation.ResponseHeadersAsync, - responseContinuation.GetStatus, - responseContinuation.GetTrailers, - responseContinuation.WithBestEffortDispose(rpcScope)); - } - catch (Exception e) - { - rpcScope?.CompleteWithException(e); - throw; - } - finally - { - rpcScope?.RestoreParentActivity(); - } + rpcScope?.CompleteWithException(e); + throw; + } + finally + { + rpcScope?.RestoreParentActivity(); } + } + + /// + public override AsyncServerStreamingCall AsyncServerStreamingCall( + TRequest request, + ClientInterceptorContext context, + AsyncServerStreamingCallContinuation continuation) + { + ClientRpcScope rpcScope = null; - /// - public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( - ClientInterceptorContext context, - AsyncDuplexStreamingCallContinuation continuation) + try + { + rpcScope = new ClientRpcScope(context, this.options); + rpcScope.RecordRequest(request); + var responseContinuation = continuation(request, rpcScope.Context); + + var responseStreamProxy = new AsyncStreamReaderProxy( + responseContinuation.ResponseStream, + rpcScope.RecordResponse, + rpcScope.Complete, + rpcScope.CompleteWithException); + + return new AsyncServerStreamingCall( + responseStreamProxy, + responseContinuation.ResponseHeadersAsync, + responseContinuation.GetStatus, + responseContinuation.GetTrailers, + responseContinuation.WithBestEffortDispose(rpcScope)); + } + catch (Exception e) + { + rpcScope?.CompleteWithException(e); + throw; + } + finally { - ClientRpcScope rpcScope = null; + rpcScope?.RestoreParentActivity(); + } + } - try - { - rpcScope = new ClientRpcScope(context, this.options); - var responseContinuation = continuation(rpcScope.Context); - - var requestStreamProxy = new ClientStreamWriterProxy( - responseContinuation.RequestStream, - rpcScope.RecordRequest, - onException: rpcScope.CompleteWithException); - - var responseStreamProxy = new AsyncStreamReaderProxy( - responseContinuation.ResponseStream, - rpcScope.RecordResponse, - rpcScope.Complete, - rpcScope.CompleteWithException); - - return new AsyncDuplexStreamingCall( - requestStreamProxy, - responseStreamProxy, - responseContinuation.ResponseHeadersAsync, - responseContinuation.GetStatus, - responseContinuation.GetTrailers, - responseContinuation.WithBestEffortDispose(rpcScope)); - } - catch (Exception e) - { - rpcScope?.CompleteWithException(e); - throw; - } - finally - { - rpcScope?.RestoreParentActivity(); - } + /// + public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( + ClientInterceptorContext context, + AsyncDuplexStreamingCallContinuation continuation) + { + ClientRpcScope rpcScope = null; + + try + { + rpcScope = new ClientRpcScope(context, this.options); + var responseContinuation = continuation(rpcScope.Context); + + var requestStreamProxy = new ClientStreamWriterProxy( + responseContinuation.RequestStream, + rpcScope.RecordRequest, + onException: rpcScope.CompleteWithException); + + var responseStreamProxy = new AsyncStreamReaderProxy( + responseContinuation.ResponseStream, + rpcScope.RecordResponse, + rpcScope.Complete, + rpcScope.CompleteWithException); + + return new AsyncDuplexStreamingCall( + requestStreamProxy, + responseStreamProxy, + responseContinuation.ResponseHeadersAsync, + responseContinuation.GetStatus, + responseContinuation.GetTrailers, + responseContinuation.WithBestEffortDispose(rpcScope)); + } + catch (Exception e) + { + rpcScope?.CompleteWithException(e); + throw; + } + finally + { + rpcScope?.RestoreParentActivity(); } + } + + /// + /// A class to help track the lifetime of a client-side RPC. + /// + /// The type of the request. + /// The type of the response. + private sealed class ClientRpcScope : RpcScope + where TRequest : class + where TResponse : class + { + /// + /// The metadata setter action. + /// + private static readonly Action MetadataSetter = (metadata, key, value) => { metadata.Add(new Metadata.Entry(key, value)); }; /// - /// A class to help track the lifetime of a client-side RPC. + /// The context. /// - /// The type of the request. - /// The type of the response. - private sealed class ClientRpcScope : RpcScope - where TRequest : class - where TResponse : class + private readonly ClientInterceptorContext context; + + /// + /// The parent activity. + /// + private readonly Activity parentActivity; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The options. + public ClientRpcScope(ClientInterceptorContext context, ClientTracingInterceptorOptions options) + : base(context.Method?.FullName, options.RecordMessageEvents) { - /// - /// The metadata setter action. - /// - private static readonly Action MetadataSetter = (metadata, key, value) => { metadata.Add(new Metadata.Entry(key, value)); }; - - /// - /// The context. - /// - private readonly ClientInterceptorContext context; - - /// - /// The parent activity. - /// - private readonly Activity parentActivity; - - /// - /// Initializes a new instance of the class. - /// - /// The context. - /// The options. - public ClientRpcScope(ClientInterceptorContext context, ClientTracingInterceptorOptions options) - : base(context.Method?.FullName, options.RecordMessageEvents) - { - this.context = context; + this.context = context; - // Capture the current activity. - this.parentActivity = Activity.Current; + // Capture the current activity. + this.parentActivity = Activity.Current; - // Short-circuit if nobody is listening - if (!GrpcCoreInstrumentation.ActivitySource.HasListeners()) - { - return; - } + // Short-circuit if nobody is listening + if (!GrpcCoreInstrumentation.ActivitySource.HasListeners()) + { + return; + } - // This if block is for unit testing only. - IEnumerable> customTags = null; - if (options.ActivityIdentifierValue != default) + // This if block is for unit testing only. + IEnumerable> customTags = null; + if (options.ActivityIdentifierValue != default) + { + customTags = new List> { - customTags = new List> - { - new KeyValuePair(SemanticConventions.AttributeActivityIdentifier, options.ActivityIdentifierValue), - }; - } + new KeyValuePair(SemanticConventions.AttributeActivityIdentifier, options.ActivityIdentifierValue), + }; + } - // We want to start an activity but don't activate it. - // After calling StartActivity, Activity.Current will be the new Activity. - // This scope is created synchronously before the RPC invocation starts and so this new Activity will overwrite - // the callers current Activity which isn't what we want. We need to restore the original immediately after doing this. - // If this call happened after some kind of async context await then a restore wouldn't be necessary. - // gRPC Core just doesn't have the hooks to do this as far as I can tell. - var rpcActivity = GrpcCoreInstrumentation.ActivitySource.StartActivity( - this.FullServiceName, - ActivityKind.Client, - this.parentActivity == default ? default : this.parentActivity.Context, - tags: customTags); - - if (rpcActivity == null) - { - return; - } + // We want to start an activity but don't activate it. + // After calling StartActivity, Activity.Current will be the new Activity. + // This scope is created synchronously before the RPC invocation starts and so this new Activity will overwrite + // the callers current Activity which isn't what we want. We need to restore the original immediately after doing this. + // If this call happened after some kind of async context await then a restore wouldn't be necessary. + // gRPC Core just doesn't have the hooks to do this as far as I can tell. + var rpcActivity = GrpcCoreInstrumentation.ActivitySource.StartActivity( + this.FullServiceName, + ActivityKind.Client, + this.parentActivity == default ? default : this.parentActivity.Context, + tags: customTags); + + if (rpcActivity == null) + { + return; + } - var callOptions = context.Options; + var callOptions = context.Options; - // Do NOT mutate incoming call headers, make a new copy. - // Retry mechanisms that may sit above this interceptor rely on an original set of call headers. - var metadata = new Metadata(); - if (callOptions.Headers != null) + // Do NOT mutate incoming call headers, make a new copy. + // Retry mechanisms that may sit above this interceptor rely on an original set of call headers. + var metadata = new Metadata(); + if (callOptions.Headers != null) + { + for (var i = 0; i < callOptions.Headers.Count; i++) { - for (var i = 0; i < callOptions.Headers.Count; i++) - { - metadata.Add(callOptions.Headers[i]); - } + metadata.Add(callOptions.Headers[i]); } + } - // replace the CallOptions - callOptions = callOptions.WithHeaders(metadata); + // replace the CallOptions + callOptions = callOptions.WithHeaders(metadata); - this.SetActivity(rpcActivity); - options.Propagator.Inject(new PropagationContext(rpcActivity.Context, Baggage.Current), callOptions.Headers, MetadataSetter); - this.context = new ClientInterceptorContext(context.Method, context.Host, callOptions); - } + this.SetActivity(rpcActivity); + options.Propagator.Inject(new PropagationContext(rpcActivity.Context, Baggage.Current), callOptions.Headers, MetadataSetter); + this.context = new ClientInterceptorContext(context.Method, context.Host, callOptions); + } - /// - /// Gets the context. - /// - public ClientInterceptorContext Context => this.context; + /// + /// Gets the context. + /// + public ClientInterceptorContext Context => this.context; - /// - /// Restores the parent activity. - /// - public void RestoreParentActivity() - { - Activity.Current = this.parentActivity; - } + /// + /// Restores the parent activity. + /// + public void RestoreParentActivity() + { + Activity.Current = this.parentActivity; } } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptorOptions.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptorOptions.cs index 1eac18fe586..5e7898b58de 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptorOptions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/ClientTracingInterceptorOptions.cs @@ -14,29 +14,28 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore -{ - using System; - using OpenTelemetry.Context.Propagation; +using System; +using OpenTelemetry.Context.Propagation; + +namespace OpenTelemetry.Instrumentation.GrpcCore; +/// +/// Options for the ClientTracingInterceptor. +/// +public class ClientTracingInterceptorOptions +{ /// - /// Options for the ClientTracingInterceptor. + /// Gets or sets a value indicating whether or not to record individual message events. /// - public class ClientTracingInterceptorOptions - { - /// - /// Gets or sets a value indicating whether or not to record individual message events. - /// - public bool RecordMessageEvents { get; set; } = false; + public bool RecordMessageEvents { get; set; } = false; - /// - /// Gets the propagator. - /// - public TextMapPropagator Propagator { get; internal set; } = Propagators.DefaultTextMapPropagator; + /// + /// Gets the propagator. + /// + public TextMapPropagator Propagator { get; internal set; } = Propagators.DefaultTextMapPropagator; - /// - /// Gets or sets a custom identfier used during unit testing. - /// - internal Guid ActivityIdentifierValue { get; set; } - } + /// + /// Gets or sets a custom identfier used during unit testing. + /// + internal Guid ActivityIdentifierValue { get; set; } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/Extensions.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/Extensions.cs index 0d31f0c8f3c..ff4ee15c88f 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/Extensions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/Extensions.cs @@ -14,34 +14,33 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore -{ - using System; +using System; + +namespace OpenTelemetry.Instrumentation.GrpcCore; +/// +/// Other useful extensions. +/// +internal static class Extensions +{ /// - /// Other useful extensions. + /// Builds an Action comprised of two calls to Dispose with best effort execution for the second disposable. /// - internal static class Extensions + /// The first. + /// The second. + /// An Action. + internal static Action WithBestEffortDispose(this IDisposable first, IDisposable second) { - /// - /// Builds an Action comprised of two calls to Dispose with best effort execution for the second disposable. - /// - /// The first. - /// The second. - /// An Action. - internal static Action WithBestEffortDispose(this IDisposable first, IDisposable second) + return () => { - return () => + try + { + first.Dispose(); + } + finally { - try - { - first.Dispose(); - } - finally - { - second.Dispose(); - } - }; - } + second.Dispose(); + } + }; } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/GrpcCoreInstrumentation.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/GrpcCoreInstrumentation.cs index f215aa9a0a7..0c7a6a80c03 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/GrpcCoreInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/GrpcCoreInstrumentation.cs @@ -14,35 +14,34 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore -{ - using System; - using System.Diagnostics; - using System.Reflection; +using System; +using System.Diagnostics; +using System.Reflection; + +namespace OpenTelemetry.Instrumentation.GrpcCore; +/// +/// Instrumentation class Grpc.Core. +/// +internal static class GrpcCoreInstrumentation +{ /// - /// Instrumentation class Grpc.Core. + /// The assembly name. /// - internal static class GrpcCoreInstrumentation - { - /// - /// The assembly name. - /// - internal static readonly AssemblyName AssemblyName = typeof(GrpcCoreInstrumentation).Assembly.GetName(); + internal static readonly AssemblyName AssemblyName = typeof(GrpcCoreInstrumentation).Assembly.GetName(); - /// - /// The activity source name. - /// - internal static readonly string ActivitySourceName = AssemblyName.Name; + /// + /// The activity source name. + /// + internal static readonly string ActivitySourceName = AssemblyName.Name; - /// - /// The version. - /// - internal static readonly Version Version = AssemblyName.Version; + /// + /// The version. + /// + internal static readonly Version Version = AssemblyName.Version; - /// - /// The activity source. - /// - internal static readonly ActivitySource ActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); - } + /// + /// The activity source. + /// + internal static readonly ActivitySource ActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/RpcScope.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/RpcScope.cs index 1abdf007012..e179a194542 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/RpcScope.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/RpcScope.cs @@ -14,218 +14,217 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using global::Grpc.Core; +using Google.Protobuf; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Instrumentation.GrpcCore; + +/// +/// A class to help track the lifetime of an RPC. +/// +/// The type of the request. +/// The type of the response. +internal abstract class RpcScope : IDisposable + where TRequest : class + where TResponse : class { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Threading; - using global::Grpc.Core; - using Google.Protobuf; - using OpenTelemetry.Trace; + /// + /// The record message events flag. + /// + private readonly bool recordMessageEvents; /// - /// A class to help track the lifetime of an RPC. + /// The RPC activity. /// - /// The type of the request. - /// The type of the response. - internal abstract class RpcScope : IDisposable - where TRequest : class - where TResponse : class - { - /// - /// The record message events flag. - /// - private readonly bool recordMessageEvents; - - /// - /// The RPC activity. - /// - private Activity activity; - - /// - /// The complete flag. - /// - private long complete = 0; - - /// - /// The request message counter. - /// - private int requestMessageCounter; - - /// - /// The response counter. - /// - private int responseMessageCounter; - - /// - /// Initializes a new instance of the class. - /// - /// Full name of the service. - /// if set to true [record message events]. - protected RpcScope(string fullServiceName, bool recordMessageEvents) - { - this.FullServiceName = fullServiceName?.TrimStart('/') ?? "unknownservice/unknownmethod"; - this.recordMessageEvents = recordMessageEvents; - } + private Activity activity; - /// - /// Gets the full name of the service. - /// - protected string FullServiceName { get; } + /// + /// The complete flag. + /// + private long complete = 0; - /// - /// Records a request message. - /// - /// The request. - public void RecordRequest(TRequest request) - { - this.requestMessageCounter++; + /// + /// The request message counter. + /// + private int requestMessageCounter; - if (this.activity == null || !this.activity.IsAllDataRequested || !this.recordMessageEvents) - { - return; - } + /// + /// The response counter. + /// + private int responseMessageCounter; - this.AddMessageEvent(typeof(TRequest).Name, request as IMessage, request: true); - } + /// + /// Initializes a new instance of the class. + /// + /// Full name of the service. + /// if set to true [record message events]. + protected RpcScope(string fullServiceName, bool recordMessageEvents) + { + this.FullServiceName = fullServiceName?.TrimStart('/') ?? "unknownservice/unknownmethod"; + this.recordMessageEvents = recordMessageEvents; + } - /// - /// Records a response message. - /// - /// The response. - public void RecordResponse(TResponse response) - { - this.responseMessageCounter++; + /// + /// Gets the full name of the service. + /// + protected string FullServiceName { get; } - if (this.activity == null || !this.activity.IsAllDataRequested || !this.recordMessageEvents) - { - return; - } + /// + /// Records a request message. + /// + /// The request. + public void RecordRequest(TRequest request) + { + this.requestMessageCounter++; - this.AddMessageEvent(typeof(TResponse).Name, response as IMessage, request: false); + if (this.activity == null || !this.activity.IsAllDataRequested || !this.recordMessageEvents) + { + return; } - /// - /// Completes the RPC. - /// - public void Complete() - { - if (this.activity == null) - { - return; - } + this.AddMessageEvent(typeof(TRequest).Name, request as IMessage, request: true); + } - // The overall Span status should remain unset however the grpc status code attribute is required - this.StopActivity((int)Grpc.Core.StatusCode.OK); - } + /// + /// Records a response message. + /// + /// The response. + public void RecordResponse(TResponse response) + { + this.responseMessageCounter++; - /// - /// Records a failed RPC. - /// - /// The exception. - public void CompleteWithException(Exception exception) + if (this.activity == null || !this.activity.IsAllDataRequested || !this.recordMessageEvents) { - if (this.activity == null) - { - return; - } - - var grpcStatusCode = Grpc.Core.StatusCode.Unknown; - var description = exception.Message; + return; + } - if (exception is RpcException rpcException) - { - grpcStatusCode = rpcException.StatusCode; - description = rpcException.Message; - } + this.AddMessageEvent(typeof(TResponse).Name, response as IMessage, request: false); + } - this.StopActivity((int)grpcStatusCode, description); + /// + /// Completes the RPC. + /// + public void Complete() + { + if (this.activity == null) + { + return; } - /// - public void Dispose() + // The overall Span status should remain unset however the grpc status code attribute is required + this.StopActivity((int)Grpc.Core.StatusCode.OK); + } + + /// + /// Records a failed RPC. + /// + /// The exception. + public void CompleteWithException(Exception exception) + { + if (this.activity == null) { - if (this.activity == null) - { - return; - } + return; + } - // If not already completed this will mark the Activity as cancelled. - this.StopActivity((int)Grpc.Core.StatusCode.Cancelled); + var grpcStatusCode = Grpc.Core.StatusCode.Unknown; + var description = exception.Message; + + if (exception is RpcException rpcException) + { + grpcStatusCode = rpcException.StatusCode; + description = rpcException.Message; } - /// - /// Sets the activity for this RPC scope. Should only be called once. - /// - /// The activity. - protected void SetActivity(Activity activity) + this.StopActivity((int)grpcStatusCode, description); + } + + /// + public void Dispose() + { + if (this.activity == null) { - this.activity = activity; + return; + } - if (this.activity == null || !this.activity.IsAllDataRequested) - { - return; - } + // If not already completed this will mark the Activity as cancelled. + this.StopActivity((int)Grpc.Core.StatusCode.Cancelled); + } + + /// + /// Sets the activity for this RPC scope. Should only be called once. + /// + /// The activity. + protected void SetActivity(Activity activity) + { + this.activity = activity; - // assign some reasonable defaults - var rpcService = this.FullServiceName; - var rpcMethod = this.FullServiceName; + if (this.activity == null || !this.activity.IsAllDataRequested) + { + return; + } - // split the full service name by the slash - var parts = this.FullServiceName.Split('/'); - if (parts.Length == 2) - { - rpcService = parts[0]; - rpcMethod = parts[1]; - } + // assign some reasonable defaults + var rpcService = this.FullServiceName; + var rpcMethod = this.FullServiceName; - this.activity.SetTag(SemanticConventions.AttributeRpcSystem, "grpc"); - this.activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); - this.activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); + // split the full service name by the slash + var parts = this.FullServiceName.Split('/'); + if (parts.Length == 2) + { + rpcService = parts[0]; + rpcMethod = parts[1]; } - /// - /// Stops the activity. - /// - /// The status code. - /// The description, if any. - private void StopActivity(int statusCode, string statusDescription = null) + this.activity.SetTag(SemanticConventions.AttributeRpcSystem, "grpc"); + this.activity.SetTag(SemanticConventions.AttributeRpcService, rpcService); + this.activity.SetTag(SemanticConventions.AttributeRpcMethod, rpcMethod); + } + + /// + /// Stops the activity. + /// + /// The status code. + /// The description, if any. + private void StopActivity(int statusCode, string statusDescription = null) + { + if (Interlocked.CompareExchange(ref this.complete, 1, 0) == 0) { - if (Interlocked.CompareExchange(ref this.complete, 1, 0) == 0) + this.activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, statusCode); + if (statusDescription != null) { - this.activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, statusCode); - if (statusDescription != null) - { - this.activity.SetStatus(OpenTelemetry.Trace.Status.Error.WithDescription(statusDescription)); - } - - this.activity.Stop(); + this.activity.SetStatus(OpenTelemetry.Trace.Status.Error.WithDescription(statusDescription)); } + + this.activity.Stop(); } + } - /// - /// Adds a message event. - /// - /// Name of the event. - /// The message. - /// if true this is a request message. - private void AddMessageEvent(string eventName, IMessage message, bool request) - { - var messageSize = message.CalculateSize(); + /// + /// Adds a message event. + /// + /// Name of the event. + /// The message. + /// if true this is a request message. + private void AddMessageEvent(string eventName, IMessage message, bool request) + { + var messageSize = message.CalculateSize(); - var attributes = new ActivityTagsCollection(new KeyValuePair[5] - { - new KeyValuePair("name", "message"), - new KeyValuePair(SemanticConventions.AttributeMessageType, request ? "SENT" : "RECEIVED"), - new KeyValuePair(SemanticConventions.AttributeMessageID, request ? this.requestMessageCounter : this.responseMessageCounter), + var attributes = new ActivityTagsCollection(new KeyValuePair[5] + { + new KeyValuePair("name", "message"), + new KeyValuePair(SemanticConventions.AttributeMessageType, request ? "SENT" : "RECEIVED"), + new KeyValuePair(SemanticConventions.AttributeMessageID, request ? this.requestMessageCounter : this.responseMessageCounter), - // TODO how to get the real compressed or uncompressed sizes - new KeyValuePair(SemanticConventions.AttributeMessageCompressedSize, messageSize), - new KeyValuePair(SemanticConventions.AttributeMessageUncompressedSize, messageSize), - }); + // TODO how to get the real compressed or uncompressed sizes + new KeyValuePair(SemanticConventions.AttributeMessageCompressedSize, messageSize), + new KeyValuePair(SemanticConventions.AttributeMessageUncompressedSize, messageSize), + }); - this.activity.AddEvent(new ActivityEvent(eventName, default, attributes)); - } + this.activity.AddEvent(new ActivityEvent(eventName, default, attributes)); } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/SemanticConventions.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/SemanticConventions.cs index be314c854ab..3746e3fb60d 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/SemanticConventions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/SemanticConventions.cs @@ -14,27 +14,26 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore +namespace OpenTelemetry.Instrumentation.GrpcCore; + +/// +/// Semantic conventions. +/// +internal static class SemanticConventions { - /// - /// Semantic conventions. - /// - internal static class SemanticConventions - { #pragma warning disable SA1600 // Elements should be documented - public const string AttributeRpcSystem = "rpc.system"; - public const string AttributeRpcService = "rpc.service"; - public const string AttributeRpcMethod = "rpc.method"; - public const string AttributeRpcGrpcStatusCode = "rpc.grpc.status_code"; - public const string AttributeMessageType = "message.type"; - public const string AttributeMessageID = "message.id"; - public const string AttributeMessageCompressedSize = "message.compressed_size"; - public const string AttributeMessageUncompressedSize = "message.uncompressed_size"; - public const string AttributeOtelStatusCode = "otel.status_code"; - public const string AttributeOtelStatusDescription = "otel.status_description"; + public const string AttributeRpcSystem = "rpc.system"; + public const string AttributeRpcService = "rpc.service"; + public const string AttributeRpcMethod = "rpc.method"; + public const string AttributeRpcGrpcStatusCode = "rpc.grpc.status_code"; + public const string AttributeMessageType = "message.type"; + public const string AttributeMessageID = "message.id"; + public const string AttributeMessageCompressedSize = "message.compressed_size"; + public const string AttributeMessageUncompressedSize = "message.uncompressed_size"; + public const string AttributeOtelStatusCode = "otel.status_code"; + public const string AttributeOtelStatusDescription = "otel.status_description"; - // Used for unit testing only. - internal const string AttributeActivityIdentifier = "activityidentifier"; + // Used for unit testing only. + internal const string AttributeActivityIdentifier = "activityidentifier"; #pragma warning restore SA1600 // Elements should be documented - } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/ServerStreamWriterProxy.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/ServerStreamWriterProxy.cs index b4a1bc18242..8f9abc417c8 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/ServerStreamWriterProxy.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/ServerStreamWriterProxy.cs @@ -14,56 +14,55 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore -{ - using System; - using System.Threading.Tasks; - using global::Grpc.Core; +using System; +using System.Threading.Tasks; +using global::Grpc.Core; + +namespace OpenTelemetry.Instrumentation.GrpcCore; +/// +/// A proxy server stream writer. +/// +/// +/// Borrowed heavily from +/// https://github.com/opentracing-contrib/csharp-grpc/blob/master/src/OpenTracing.Contrib.Grpc/Streaming/TracingServerStreamWriter.cs. +/// +/// The message type. +/// +internal class ServerStreamWriterProxy : IServerStreamWriter +{ /// - /// A proxy server stream writer. + /// The writer. /// - /// - /// Borrowed heavily from - /// https://github.com/opentracing-contrib/csharp-grpc/blob/master/src/OpenTracing.Contrib.Grpc/Streaming/TracingServerStreamWriter.cs. - /// - /// The message type. - /// - internal class ServerStreamWriterProxy : IServerStreamWriter - { - /// - /// The writer. - /// - private readonly IServerStreamWriter writer; + private readonly IServerStreamWriter writer; - /// - /// The on write action. - /// - private readonly Action onWrite; + /// + /// The on write action. + /// + private readonly Action onWrite; - /// - /// Initializes a new instance of the class. - /// - /// The writer. - /// The on write action, if any. - public ServerStreamWriterProxy(IServerStreamWriter writer, Action onWrite = null) - { - this.writer = writer; - this.onWrite = onWrite; - } + /// + /// Initializes a new instance of the class. + /// + /// The writer. + /// The on write action, if any. + public ServerStreamWriterProxy(IServerStreamWriter writer, Action onWrite = null) + { + this.writer = writer; + this.onWrite = onWrite; + } - /// - public WriteOptions WriteOptions - { - get => this.writer.WriteOptions; - set => this.writer.WriteOptions = value; - } + /// + public WriteOptions WriteOptions + { + get => this.writer.WriteOptions; + set => this.writer.WriteOptions = value; + } - /// - public Task WriteAsync(T message) - { - this.onWrite?.Invoke(message); - return this.writer.WriteAsync(message); - } + /// + public Task WriteAsync(T message) + { + this.onWrite?.Invoke(message); + return this.writer.WriteAsync(message); } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptor.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptor.cs index 5dd4633306d..cf6ff034673 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptor.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptor.cs @@ -14,216 +14,215 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using global::Grpc.Core; +using global::Grpc.Core.Interceptors; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.GrpcCore; + +/// +/// A service interceptor that starts and stops an Activity for each inbound RPC. +/// +/// +public class ServerTracingInterceptor : Interceptor { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Threading.Tasks; - using global::Grpc.Core; - using global::Grpc.Core.Interceptors; - using OpenTelemetry.Context.Propagation; - using OpenTelemetry.Internal; + /// + /// The options. + /// + private readonly ServerTracingInterceptorOptions options; /// - /// A service interceptor that starts and stops an Activity for each inbound RPC. + /// Initializes a new instance of the class. /// - /// - public class ServerTracingInterceptor : Interceptor + /// The options. + public ServerTracingInterceptor(ServerTracingInterceptorOptions options) { - /// - /// The options. - /// - private readonly ServerTracingInterceptorOptions options; + Guard.ThrowIfNull(options); - /// - /// Initializes a new instance of the class. - /// - /// The options. - public ServerTracingInterceptor(ServerTracingInterceptorOptions options) - { - Guard.ThrowIfNull(options); + this.options = options; + } - this.options = options; - } + /// + public override async Task UnaryServerHandler( + TRequest request, + ServerCallContext context, + UnaryServerMethod continuation) + { + using var rpcScope = new ServerRpcScope(context, this.options); - /// - public override async Task UnaryServerHandler( - TRequest request, - ServerCallContext context, - UnaryServerMethod continuation) + try { - using var rpcScope = new ServerRpcScope(context, this.options); - - try - { - rpcScope.RecordRequest(request); - var response = await continuation(request, context).ConfigureAwait(false); - rpcScope.RecordResponse(response); - rpcScope.Complete(); - return response; - } - catch (Exception e) - { - rpcScope.CompleteWithException(e); - throw; - } + rpcScope.RecordRequest(request); + var response = await continuation(request, context).ConfigureAwait(false); + rpcScope.RecordResponse(response); + rpcScope.Complete(); + return response; } - - /// - public override async Task ClientStreamingServerHandler( - IAsyncStreamReader requestStream, - ServerCallContext context, - ClientStreamingServerMethod continuation) + catch (Exception e) { - using var rpcScope = new ServerRpcScope(context, this.options); - - try - { - var requestStreamReaderProxy = new AsyncStreamReaderProxy( - requestStream, - rpcScope.RecordRequest); - - var response = await continuation(requestStreamReaderProxy, context).ConfigureAwait(false); - rpcScope.RecordResponse(response); - rpcScope.Complete(); - return response; - } - catch (Exception e) - { - rpcScope.CompleteWithException(e); - throw; - } + rpcScope.CompleteWithException(e); + throw; } + } + + /// + public override async Task ClientStreamingServerHandler( + IAsyncStreamReader requestStream, + ServerCallContext context, + ClientStreamingServerMethod continuation) + { + using var rpcScope = new ServerRpcScope(context, this.options); - /// - public override async Task ServerStreamingServerHandler( - TRequest request, - IServerStreamWriter responseStream, - ServerCallContext context, - ServerStreamingServerMethod continuation) + try { - using var rpcScope = new ServerRpcScope(context, this.options); + var requestStreamReaderProxy = new AsyncStreamReaderProxy( + requestStream, + rpcScope.RecordRequest); + + var response = await continuation(requestStreamReaderProxy, context).ConfigureAwait(false); + rpcScope.RecordResponse(response); + rpcScope.Complete(); + return response; + } + catch (Exception e) + { + rpcScope.CompleteWithException(e); + throw; + } + } - try - { - rpcScope.RecordRequest(request); + /// + public override async Task ServerStreamingServerHandler( + TRequest request, + IServerStreamWriter responseStream, + ServerCallContext context, + ServerStreamingServerMethod continuation) + { + using var rpcScope = new ServerRpcScope(context, this.options); - var responseStreamProxy = new ServerStreamWriterProxy( - responseStream, - rpcScope.RecordResponse); + try + { + rpcScope.RecordRequest(request); - await continuation(request, responseStreamProxy, context).ConfigureAwait(false); - rpcScope.Complete(); - } - catch (Exception e) - { - rpcScope.CompleteWithException(e); - throw; - } - } + var responseStreamProxy = new ServerStreamWriterProxy( + responseStream, + rpcScope.RecordResponse); - /// - public override async Task DuplexStreamingServerHandler(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context, DuplexStreamingServerMethod continuation) + await continuation(request, responseStreamProxy, context).ConfigureAwait(false); + rpcScope.Complete(); + } + catch (Exception e) { - using var rpcScope = new ServerRpcScope(context, this.options); + rpcScope.CompleteWithException(e); + throw; + } + } - try - { - var requestStreamReaderProxy = new AsyncStreamReaderProxy( - requestStream, - rpcScope.RecordRequest); + /// + public override async Task DuplexStreamingServerHandler(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context, DuplexStreamingServerMethod continuation) + { + using var rpcScope = new ServerRpcScope(context, this.options); - var responseStreamProxy = new ServerStreamWriterProxy( - responseStream, - rpcScope.RecordResponse); + try + { + var requestStreamReaderProxy = new AsyncStreamReaderProxy( + requestStream, + rpcScope.RecordRequest); - await continuation(requestStreamReaderProxy, responseStreamProxy, context).ConfigureAwait(false); - rpcScope.Complete(); - } - catch (Exception e) - { - rpcScope.CompleteWithException(e); - throw; - } + var responseStreamProxy = new ServerStreamWriterProxy( + responseStream, + rpcScope.RecordResponse); + + await continuation(requestStreamReaderProxy, responseStreamProxy, context).ConfigureAwait(false); + rpcScope.Complete(); + } + catch (Exception e) + { + rpcScope.CompleteWithException(e); + throw; } + } + /// + /// A class to help track the lifetime of a service-side RPC. + /// + /// The type of the request. + /// The type of the response. + private class ServerRpcScope : RpcScope + where TRequest : class + where TResponse : class + { /// - /// A class to help track the lifetime of a service-side RPC. + /// The metadata setter action. /// - /// The type of the request. - /// The type of the response. - private class ServerRpcScope : RpcScope - where TRequest : class - where TResponse : class + private static readonly Func> MetadataGetter = (metadata, key) => { - /// - /// The metadata setter action. - /// - private static readonly Func> MetadataGetter = (metadata, key) => + for (var i = 0; i < metadata.Count; i++) { - for (var i = 0; i < metadata.Count; i++) + var entry = metadata[i]; + if (string.Equals(entry.Key, key, StringComparison.OrdinalIgnoreCase)) { - var entry = metadata[i]; - if (string.Equals(entry.Key, key, StringComparison.OrdinalIgnoreCase)) - { - return new string[1] { entry.Value }; - } + return new string[1] { entry.Value }; } + } - return Enumerable.Empty(); - }; + return Enumerable.Empty(); + }; - /// - /// Initializes a new instance of the class. - /// - /// The context. - /// The options. - public ServerRpcScope(ServerCallContext context, ServerTracingInterceptorOptions options) - : base(context.Method, options.RecordMessageEvents) + /// + /// Initializes a new instance of the class. + /// + /// The context. + /// The options. + public ServerRpcScope(ServerCallContext context, ServerTracingInterceptorOptions options) + : base(context.Method, options.RecordMessageEvents) + { + if (!GrpcCoreInstrumentation.ActivitySource.HasListeners()) { - if (!GrpcCoreInstrumentation.ActivitySource.HasListeners()) - { - return; - } + return; + } - var currentContext = Activity.Current?.Context; + var currentContext = Activity.Current?.Context; - // Extract the SpanContext, if any from the headers - var metadata = context.RequestHeaders; - if (metadata != null) + // Extract the SpanContext, if any from the headers + var metadata = context.RequestHeaders; + if (metadata != null) + { + var propagationContext = options.Propagator.Extract(new PropagationContext(currentContext ?? default, Baggage.Current), metadata, MetadataGetter); + if (propagationContext.ActivityContext.IsValid()) { - var propagationContext = options.Propagator.Extract(new PropagationContext(currentContext ?? default, Baggage.Current), metadata, MetadataGetter); - if (propagationContext.ActivityContext.IsValid()) - { - currentContext = propagationContext.ActivityContext; - } - - if (propagationContext.Baggage != default) - { - Baggage.Current = propagationContext.Baggage; - } + currentContext = propagationContext.ActivityContext; } - // This if block is for unit testing only. - IEnumerable> customTags = null; - if (options.ActivityIdentifierValue != default) + if (propagationContext.Baggage != default) { - customTags = new List> - { - new KeyValuePair(SemanticConventions.AttributeActivityIdentifier, options.ActivityIdentifierValue), - }; + Baggage.Current = propagationContext.Baggage; } + } - var activity = GrpcCoreInstrumentation.ActivitySource.StartActivity( - this.FullServiceName, - ActivityKind.Server, - currentContext ?? default, - tags: customTags); - - this.SetActivity(activity); + // This if block is for unit testing only. + IEnumerable> customTags = null; + if (options.ActivityIdentifierValue != default) + { + customTags = new List> + { + new KeyValuePair(SemanticConventions.AttributeActivityIdentifier, options.ActivityIdentifierValue), + }; } + + var activity = GrpcCoreInstrumentation.ActivitySource.StartActivity( + this.FullServiceName, + ActivityKind.Server, + currentContext ?? default, + tags: customTags); + + this.SetActivity(activity); } } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptorOptions.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptorOptions.cs index 37134913976..94fbea9967e 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptorOptions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/ServerTracingInterceptorOptions.cs @@ -14,29 +14,28 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.GrpcCore -{ - using System; - using OpenTelemetry.Context.Propagation; +using System; +using OpenTelemetry.Context.Propagation; + +namespace OpenTelemetry.Instrumentation.GrpcCore; +/// +/// Options for the ServerTracingInterceptor. +/// +public class ServerTracingInterceptorOptions +{ /// - /// Options for the ServerTracingInterceptor. + /// Gets or sets a value indicating whether or not to record individual message events. /// - public class ServerTracingInterceptorOptions - { - /// - /// Gets or sets a value indicating whether or not to record individual message events. - /// - public bool RecordMessageEvents { get; set; } = false; + public bool RecordMessageEvents { get; set; } = false; - /// - /// Gets the propagator. - /// - public TextMapPropagator Propagator { get; internal set; } = Propagators.DefaultTextMapPropagator; + /// + /// Gets the propagator. + /// + public TextMapPropagator Propagator { get; internal set; } = Propagators.DefaultTextMapPropagator; - /// - /// Gets or sets a custom identfier used during unit testing. - /// - internal Guid ActivityIdentifierValue { get; set; } - } + /// + /// Gets or sets a custom identfier used during unit testing. + /// + internal Guid ActivityIdentifierValue { get; set; } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcCore/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.GrpcCore/TracerProviderBuilderExtensions.cs index 2b3e8174380..3ab16404c2c 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcCore/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcCore/TracerProviderBuilderExtensions.cs @@ -14,27 +14,26 @@ // limitations under the License. // -namespace OpenTelemetry.Trace -{ - using OpenTelemetry.Instrumentation.GrpcCore; - using OpenTelemetry.Internal; +using OpenTelemetry.Instrumentation.GrpcCore; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace; +/// +/// OpenTelemetry builder extensions to simplify registration of Grpc.Core based interceptors. +/// +public static class TracerProviderBuilderExtensions +{ /// - /// OpenTelemetry builder extensions to simplify registration of Grpc.Core based interceptors. + /// Configures OpenTelemetry to listen for the Activities created by the client and server interceptors. /// - public static class TracerProviderBuilderExtensions + /// The builder. + /// The instance of to chain the calls. + public static TracerProviderBuilder AddGrpcCoreInstrumentation( + this TracerProviderBuilder builder) { - /// - /// Configures OpenTelemetry to listen for the Activities created by the client and server interceptors. - /// - /// The builder. - /// The instance of to chain the calls. - public static TracerProviderBuilder AddGrpcCoreInstrumentation( - this TracerProviderBuilder builder) - { - Guard.ThrowIfNull(builder); + Guard.ThrowIfNull(builder); - return builder.AddSource(GrpcCoreInstrumentation.ActivitySourceName); - } + return builder.AddSource(GrpcCoreInstrumentation.ActivitySourceName); } } diff --git a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/FoobarService.cs b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/FoobarService.cs index 1d5d8599e67..73f545680a8 100644 --- a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/FoobarService.cs +++ b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/FoobarService.cs @@ -23,262 +23,261 @@ using Grpc.Core; using Grpc.Core.Interceptors; -namespace OpenTelemetry.Instrumentation.GrpcCore.Test +namespace OpenTelemetry.Instrumentation.GrpcCore.Test; + +/// +/// Test implementation of foobar. +/// +internal class FoobarService : Foobar.FoobarBase { /// - /// Test implementation of foobar. + /// Default traceparent header value with the sampling bit on. /// - internal class FoobarService : Foobar.FoobarBase - { - /// - /// Default traceparent header value with the sampling bit on. - /// - internal static readonly string DefaultTraceparentWithSampling = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + internal static readonly string DefaultTraceparentWithSampling = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; - /// - /// The default parent from a traceparent header. - /// - internal static readonly ActivityContext DefaultParentFromTraceparentHeader = ActivityContext.Parse(DefaultTraceparentWithSampling, null); + /// + /// The default parent from a traceparent header. + /// + internal static readonly ActivityContext DefaultParentFromTraceparentHeader = ActivityContext.Parse(DefaultTraceparentWithSampling, null); - /// - /// The default request message. - /// - internal static readonly FoobarRequest DefaultRequestMessage = new FoobarRequest { Message = "foo" }; + /// + /// The default request message. + /// + internal static readonly FoobarRequest DefaultRequestMessage = new FoobarRequest { Message = "foo" }; - /// - /// The default request message size. - /// - internal static readonly int DefaultRequestMessageSize = ((IMessage)DefaultRequestMessage).CalculateSize(); + /// + /// The default request message size. + /// + internal static readonly int DefaultRequestMessageSize = ((IMessage)DefaultRequestMessage).CalculateSize(); - /// - /// The default response message. - /// - internal static readonly FoobarResponse DefaultResponseMessage = new FoobarResponse { Message = "bar" }; + /// + /// The default response message. + /// + internal static readonly FoobarResponse DefaultResponseMessage = new FoobarResponse { Message = "bar" }; - /// - /// The default request message size. - /// - internal static readonly int DefaultResponseMessageSize = ((IMessage)DefaultResponseMessage).CalculateSize(); + /// + /// The default request message size. + /// + internal static readonly int DefaultResponseMessageSize = ((IMessage)DefaultResponseMessage).CalculateSize(); - /// - /// The request header fail with status code. - /// - internal static readonly string RequestHeaderFailWithStatusCode = "failurestatuscode"; + /// + /// The request header fail with status code. + /// + internal static readonly string RequestHeaderFailWithStatusCode = "failurestatuscode"; - /// - /// The request header error description. - /// - internal static readonly string RequestHeaderErrorDescription = "failuredescription"; + /// + /// The request header error description. + /// + internal static readonly string RequestHeaderErrorDescription = "failuredescription"; - /// - /// Starts the specified service. - /// - /// The server interceptor. - /// A tuple. - public static DisposableServer Start(Interceptor serverInterceptor = null) + /// + /// Starts the specified service. + /// + /// The server interceptor. + /// A tuple. + public static DisposableServer Start(Interceptor serverInterceptor = null) + { + // Disable SO_REUSEPORT to prevent https://github.com/grpc/grpc/issues/10755 + var serviceDefinition = Foobar.BindService(new FoobarService()); + if (serverInterceptor != null) { - // Disable SO_REUSEPORT to prevent https://github.com/grpc/grpc/issues/10755 - var serviceDefinition = Foobar.BindService(new FoobarService()); - if (serverInterceptor != null) - { - serviceDefinition = serviceDefinition.Intercept(serverInterceptor); - } - - var server = new Server - { - Ports = { { "localhost", ServerPort.PickUnused, ServerCredentials.Insecure } }, - Services = { serviceDefinition }, - }; - - server.Start(); - var serverUriString = new Uri("dns:localhost:" + server.Ports.Single().BoundPort).ToString(); - - return new DisposableServer(server, serverUriString); + serviceDefinition = serviceDefinition.Intercept(serverInterceptor); } - /// - /// Builds the default RPC client. - /// - /// The target. - /// The client tracing interceptor, if any. - /// The additional metadata, if any. - /// - /// The gRPC client. - /// - public static Foobar.FoobarClient ConstructRpcClient( - string target, - ClientTracingInterceptor clientTracingInterceptor = null, - IEnumerable additionalMetadata = null) + var server = new Server { - var channel = new Channel(target, ChannelCredentials.Insecure); - var callInvoker = channel.CreateCallInvoker(); - - if (clientTracingInterceptor != null) - { - callInvoker = callInvoker.Intercept(clientTracingInterceptor); - } - - // The metadata injector comes first - if (additionalMetadata != null) - { - callInvoker = callInvoker.Intercept( - metadata => - { - foreach (var m in additionalMetadata) - { - metadata.Add(m); - } + Ports = { { "localhost", ServerPort.PickUnused, ServerCredentials.Insecure } }, + Services = { serviceDefinition }, + }; - return metadata; - }); - } + server.Start(); + var serverUriString = new Uri("dns:localhost:" + server.Ports.Single().BoundPort).ToString(); - return new Foobar.FoobarClient(callInvoker); - } + return new DisposableServer(server, serverUriString); + } - /// - /// Makes a unary asynchronous request. - /// - /// The client. - /// The additional metadata. - /// A Task. - public static async Task MakeUnaryAsyncRequest(Foobar.FoobarClient client, Metadata additionalMetadata) + /// + /// Builds the default RPC client. + /// + /// The target. + /// The client tracing interceptor, if any. + /// The additional metadata, if any. + /// + /// The gRPC client. + /// + public static Foobar.FoobarClient ConstructRpcClient( + string target, + ClientTracingInterceptor clientTracingInterceptor = null, + IEnumerable additionalMetadata = null) + { + var channel = new Channel(target, ChannelCredentials.Insecure); + var callInvoker = channel.CreateCallInvoker(); + + if (clientTracingInterceptor != null) { - using var call = client.UnaryAsync(DefaultRequestMessage, headers: additionalMetadata); - _ = await call.ResponseAsync.ConfigureAwait(false); + callInvoker = callInvoker.Intercept(clientTracingInterceptor); } - /// - /// Makes a client streaming request. - /// - /// The client. - /// The additional metadata. - /// A Task. - public static async Task MakeClientStreamingRequest(Foobar.FoobarClient client, Metadata additionalMetadata) + // The metadata injector comes first + if (additionalMetadata != null) { - using var call = client.ClientStreaming(headers: additionalMetadata); - await call.RequestStream.WriteAsync(DefaultRequestMessage).ConfigureAwait(false); - await call.RequestStream.CompleteAsync().ConfigureAwait(false); - _ = await call.ResponseAsync.ConfigureAwait(false); + callInvoker = callInvoker.Intercept( + metadata => + { + foreach (var m in additionalMetadata) + { + metadata.Add(m); + } + + return metadata; + }); } - /// - /// Makes a server streaming request. - /// - /// The client. - /// The additional metadata. - /// A Task. - public static async Task MakeServerStreamingRequest(Foobar.FoobarClient client, Metadata additionalMetadata) + return new Foobar.FoobarClient(callInvoker); + } + + /// + /// Makes a unary asynchronous request. + /// + /// The client. + /// The additional metadata. + /// A Task. + public static async Task MakeUnaryAsyncRequest(Foobar.FoobarClient client, Metadata additionalMetadata) + { + using var call = client.UnaryAsync(DefaultRequestMessage, headers: additionalMetadata); + _ = await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Makes a client streaming request. + /// + /// The client. + /// The additional metadata. + /// A Task. + public static async Task MakeClientStreamingRequest(Foobar.FoobarClient client, Metadata additionalMetadata) + { + using var call = client.ClientStreaming(headers: additionalMetadata); + await call.RequestStream.WriteAsync(DefaultRequestMessage).ConfigureAwait(false); + await call.RequestStream.CompleteAsync().ConfigureAwait(false); + _ = await call.ResponseAsync.ConfigureAwait(false); + } + + /// + /// Makes a server streaming request. + /// + /// The client. + /// The additional metadata. + /// A Task. + public static async Task MakeServerStreamingRequest(Foobar.FoobarClient client, Metadata additionalMetadata) + { + using var call = client.ServerStreaming(DefaultRequestMessage, headers: additionalMetadata); + while (await call.ResponseStream.MoveNext().ConfigureAwait(false)) { - using var call = client.ServerStreaming(DefaultRequestMessage, headers: additionalMetadata); - while (await call.ResponseStream.MoveNext().ConfigureAwait(false)) - { - } } + } - /// - /// Makes a duplex streaming request. - /// - /// The client. - /// The additional metadata. - /// A Task. - public static async Task MakeDuplexStreamingRequest(Foobar.FoobarClient client, Metadata additionalMetadata) - { - using var call = client.DuplexStreaming(headers: additionalMetadata); - await call.RequestStream.WriteAsync(DefaultRequestMessage).ConfigureAwait(false); - await call.RequestStream.CompleteAsync().ConfigureAwait(false); + /// + /// Makes a duplex streaming request. + /// + /// The client. + /// The additional metadata. + /// A Task. + public static async Task MakeDuplexStreamingRequest(Foobar.FoobarClient client, Metadata additionalMetadata) + { + using var call = client.DuplexStreaming(headers: additionalMetadata); + await call.RequestStream.WriteAsync(DefaultRequestMessage).ConfigureAwait(false); + await call.RequestStream.CompleteAsync().ConfigureAwait(false); - while (await call.ResponseStream.MoveNext().ConfigureAwait(false)) - { - } + while (await call.ResponseStream.MoveNext().ConfigureAwait(false)) + { } + } - /// - public override Task Unary(FoobarRequest request, ServerCallContext context) - { - this.CheckForFailure(context); + /// + public override Task Unary(FoobarRequest request, ServerCallContext context) + { + this.CheckForFailure(context); - return Task.FromResult(DefaultResponseMessage); - } + return Task.FromResult(DefaultResponseMessage); + } - /// - public override async Task ClientStreaming(IAsyncStreamReader requestStream, ServerCallContext context) + /// + public override async Task ClientStreaming(IAsyncStreamReader requestStream, ServerCallContext context) + { + this.CheckForFailure(context); + + while (await requestStream.MoveNext().ConfigureAwait(false)) { - this.CheckForFailure(context); + } - while (await requestStream.MoveNext().ConfigureAwait(false)) - { - } + return DefaultResponseMessage; + } - return DefaultResponseMessage; - } + /// + public override async Task ServerStreaming(FoobarRequest request, IServerStreamWriter responseStream, ServerCallContext context) + { + this.CheckForFailure(context); - /// - public override async Task ServerStreaming(FoobarRequest request, IServerStreamWriter responseStream, ServerCallContext context) - { - this.CheckForFailure(context); + await responseStream.WriteAsync(DefaultResponseMessage).ConfigureAwait(false); + } - await responseStream.WriteAsync(DefaultResponseMessage).ConfigureAwait(false); - } + /// + public override async Task DuplexStreaming(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + { + this.CheckForFailure(context); - /// - public override async Task DuplexStreaming(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) + while (await requestStream.MoveNext().ConfigureAwait(false)) { - this.CheckForFailure(context); + } - while (await requestStream.MoveNext().ConfigureAwait(false)) - { - } + await responseStream.WriteAsync(DefaultResponseMessage).ConfigureAwait(false); + } - await responseStream.WriteAsync(DefaultResponseMessage).ConfigureAwait(false); + /// + /// Throws if we see some by-convention request metadata. + /// + /// The context. + private void CheckForFailure(ServerCallContext context) + { + var failureStatusCodeString = context.RequestHeaders.GetValue(RequestHeaderFailWithStatusCode); + var failureDescription = context.RequestHeaders.GetValue(RequestHeaderErrorDescription); + if (failureStatusCodeString != null) + { + throw new RpcException(new Status((StatusCode)Enum.Parse(typeof(StatusCode), failureStatusCodeString), failureDescription ?? string.Empty)); } + } + + /// + /// Wraps server shutdown with an IDisposable pattern. + /// + /// + public sealed class DisposableServer : IDisposable + { + /// + /// The server. + /// + private readonly Server server; /// - /// Throws if we see some by-convention request metadata. + /// Initializes a new instance of the class. /// - /// The context. - private void CheckForFailure(ServerCallContext context) + /// The server. + /// The URI string. + public DisposableServer(Server server, string uriString) { - var failureStatusCodeString = context.RequestHeaders.GetValue(RequestHeaderFailWithStatusCode); - var failureDescription = context.RequestHeaders.GetValue(RequestHeaderErrorDescription); - if (failureStatusCodeString != null) - { - throw new RpcException(new Status((StatusCode)Enum.Parse(typeof(StatusCode), failureStatusCodeString), failureDescription ?? string.Empty)); - } + this.server = server; + this.UriString = uriString; } /// - /// Wraps server shutdown with an IDisposable pattern. + /// Gets the URI string. /// - /// - public sealed class DisposableServer : IDisposable + public string UriString { get; } + + /// + public void Dispose() { - /// - /// The server. - /// - private readonly Server server; - - /// - /// Initializes a new instance of the class. - /// - /// The server. - /// The URI string. - public DisposableServer(Server server, string uriString) - { - this.server = server; - this.UriString = uriString; - } - - /// - /// Gets the URI string. - /// - public string UriString { get; } - - /// - public void Dispose() - { - this.server.ShutdownAsync().Wait(); - } + this.server.ShutdownAsync().Wait(); } } } diff --git a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreClientInterceptorTests.cs b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreClientInterceptorTests.cs index 8becff7c7fd..56b5fb8d403 100644 --- a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreClientInterceptorTests.cs +++ b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreClientInterceptorTests.cs @@ -25,506 +25,505 @@ using OpenTelemetry.Context.Propagation; using Xunit; -namespace OpenTelemetry.Instrumentation.GrpcCore.Test +namespace OpenTelemetry.Instrumentation.GrpcCore.Test; + +/// +/// Grpc Core client interceptor tests. +/// +public class GrpcCoreClientInterceptorTests { /// - /// Grpc Core client interceptor tests. + /// A bogus server uri. + /// + private static readonly string BogusServerUri = "dns:i.dont.exist:77923"; + + /// + /// The default metadata func. + /// + private static readonly Func DefaultMetadataFunc = () => new Metadata { new Metadata.Entry("foo", "bar") }; + + /// + /// Validates a successful AsyncUnary call. /// - public class GrpcCoreClientInterceptorTests + /// A task. + [Fact] + public async Task AsyncUnarySuccess() { - /// - /// A bogus server uri. - /// - private static readonly string BogusServerUri = "dns:i.dont.exist:77923"; - - /// - /// The default metadata func. - /// - private static readonly Func DefaultMetadataFunc = () => new Metadata { new Metadata.Entry("foo", "bar") }; - - /// - /// Validates a successful AsyncUnary call. - /// - /// A task. - [Fact] - public async Task AsyncUnarySuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeUnaryAsyncRequest, DefaultMetadataFunc()).ConfigureAwait(false); - } + await this.TestHandlerSuccess(FoobarService.MakeUnaryAsyncRequest, DefaultMetadataFunc()).ConfigureAwait(false); + } - /// - /// Validates a failed AsyncUnary call because the endpoint isn't there. - /// - /// A task. - [Fact] - public async Task AsyncUnaryUnavailable() - { - await this.TestHandlerFailure( - FoobarService.MakeUnaryAsyncRequest, - StatusCode.Unavailable, - validateErrorDescription: false, - BogusServerUri).ConfigureAwait(false); - } + /// + /// Validates a failed AsyncUnary call because the endpoint isn't there. + /// + /// A task. + [Fact] + public async Task AsyncUnaryUnavailable() + { + await this.TestHandlerFailure( + FoobarService.MakeUnaryAsyncRequest, + StatusCode.Unavailable, + validateErrorDescription: false, + BogusServerUri).ConfigureAwait(false); + } + + /// + /// Validates a failed AsyncUnary call because the service returned an error. + /// + /// A task. + [Fact] + public async Task AsyncUnaryFail() + { + await this.TestHandlerFailure(FoobarService.MakeUnaryAsyncRequest).ConfigureAwait(false); + } - /// - /// Validates a failed AsyncUnary call because the service returned an error. - /// - /// A task. - [Fact] - public async Task AsyncUnaryFail() + /// + /// Validates a failed AsyncUnary call because the client is disposed before completing the RPC. + /// + [Fact] + public void AsyncUnaryDisposed() + { + static void MakeRequest(Foobar.FoobarClient client) { - await this.TestHandlerFailure(FoobarService.MakeUnaryAsyncRequest).ConfigureAwait(false); + using var call = client.UnaryAsync(FoobarService.DefaultRequestMessage); } - /// - /// Validates a failed AsyncUnary call because the client is disposed before completing the RPC. - /// - [Fact] - public void AsyncUnaryDisposed() - { - static void MakeRequest(Foobar.FoobarClient client) - { - using var call = client.UnaryAsync(FoobarService.DefaultRequestMessage); - } + this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); + } - this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); - } + /// + /// Validates a successful ClientStreaming call. + /// + /// A task. + [Fact] + public async Task ClientStreamingSuccess() + { + await this.TestHandlerSuccess(FoobarService.MakeClientStreamingRequest, DefaultMetadataFunc()).ConfigureAwait(false); + } - /// - /// Validates a successful ClientStreaming call. - /// - /// A task. - [Fact] - public async Task ClientStreamingSuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeClientStreamingRequest, DefaultMetadataFunc()).ConfigureAwait(false); - } + /// + /// Validates a failed ClientStreaming call when the service is unavailable. + /// + /// A task. + [Fact] + public async Task ClientStreamingUnavailable() + { + await this.TestHandlerFailure( + FoobarService.MakeClientStreamingRequest, + StatusCode.Unavailable, + validateErrorDescription: false, + BogusServerUri).ConfigureAwait(false); + } - /// - /// Validates a failed ClientStreaming call when the service is unavailable. - /// - /// A task. - [Fact] - public async Task ClientStreamingUnavailable() - { - await this.TestHandlerFailure( - FoobarService.MakeClientStreamingRequest, - StatusCode.Unavailable, - validateErrorDescription: false, - BogusServerUri).ConfigureAwait(false); - } + /// + /// Validates a failed ClientStreaming call. + /// + /// A task. + [Fact] + public async Task ClientStreamingFail() + { + await this.TestHandlerFailure(FoobarService.MakeClientStreamingRequest).ConfigureAwait(false); + } - /// - /// Validates a failed ClientStreaming call. - /// - /// A task. - [Fact] - public async Task ClientStreamingFail() + /// + /// Validates a failed ClientStreaming call because the client is disposed before completing the RPC. + /// + [Fact] + public void ClientStreamingDisposed() + { + static void MakeRequest(Foobar.FoobarClient client) { - await this.TestHandlerFailure(FoobarService.MakeClientStreamingRequest).ConfigureAwait(false); + using var call = client.ClientStreaming(); } - /// - /// Validates a failed ClientStreaming call because the client is disposed before completing the RPC. - /// - [Fact] - public void ClientStreamingDisposed() - { - static void MakeRequest(Foobar.FoobarClient client) - { - using var call = client.ClientStreaming(); - } + this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); + } - this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); - } + /// + /// Validates a successful ServerStreaming call. + /// + /// A task. + [Fact] + public async Task ServerStreamingSuccess() + { + await this.TestHandlerSuccess(FoobarService.MakeServerStreamingRequest, DefaultMetadataFunc()).ConfigureAwait(false); + } - /// - /// Validates a successful ServerStreaming call. - /// - /// A task. - [Fact] - public async Task ServerStreamingSuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeServerStreamingRequest, DefaultMetadataFunc()).ConfigureAwait(false); - } + /// + /// Validates a failed ServerStreaming call. + /// + /// A task. + [Fact] + public async Task ServerStreamingFail() + { + await this.TestHandlerFailure(FoobarService.MakeServerStreamingRequest).ConfigureAwait(false); + } - /// - /// Validates a failed ServerStreaming call. - /// - /// A task. - [Fact] - public async Task ServerStreamingFail() + /// + /// Validates a failed ServerStreaming call because the client is disposed before completing the RPC. + /// + [Fact] + public void ServerStreamingDisposed() + { + static void MakeRequest(Foobar.FoobarClient client) { - await this.TestHandlerFailure(FoobarService.MakeServerStreamingRequest).ConfigureAwait(false); + using var call = client.ServerStreaming(FoobarService.DefaultRequestMessage); } - /// - /// Validates a failed ServerStreaming call because the client is disposed before completing the RPC. - /// - [Fact] - public void ServerStreamingDisposed() - { - static void MakeRequest(Foobar.FoobarClient client) - { - using var call = client.ServerStreaming(FoobarService.DefaultRequestMessage); - } + this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); + } - this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); - } + /// + /// Validates a successful DuplexStreaming call. + /// + /// A task. + [Fact] + public async Task DuplexStreamingSuccess() + { + await this.TestHandlerSuccess(FoobarService.MakeDuplexStreamingRequest, DefaultMetadataFunc()).ConfigureAwait(false); + } - /// - /// Validates a successful DuplexStreaming call. - /// - /// A task. - [Fact] - public async Task DuplexStreamingSuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeDuplexStreamingRequest, DefaultMetadataFunc()).ConfigureAwait(false); - } + /// + /// Validates a failed DuplexStreaming call when the service is unavailable. + /// + /// A task. + [Fact] + public async Task DuplexStreamingUnavailable() + { + await this.TestHandlerFailure( + FoobarService.MakeDuplexStreamingRequest, + StatusCode.Unavailable, + validateErrorDescription: false, + BogusServerUri).ConfigureAwait(false); + } - /// - /// Validates a failed DuplexStreaming call when the service is unavailable. - /// - /// A task. - [Fact] - public async Task DuplexStreamingUnavailable() - { - await this.TestHandlerFailure( - FoobarService.MakeDuplexStreamingRequest, - StatusCode.Unavailable, - validateErrorDescription: false, - BogusServerUri).ConfigureAwait(false); - } + /// + /// Validates a failed DuplexStreaming call. + /// + /// A task. + [Fact] + public async Task DuplexStreamingFail() + { + await this.TestHandlerFailure(FoobarService.MakeDuplexStreamingRequest).ConfigureAwait(false); + } - /// - /// Validates a failed DuplexStreaming call. - /// - /// A task. - [Fact] - public async Task DuplexStreamingFail() + /// + /// Validates a failed DuplexStreaming call because the client is disposed before completing the RPC. + /// + [Fact] + public void DuplexStreamingDisposed() + { + static void MakeRequest(Foobar.FoobarClient client) { - await this.TestHandlerFailure(FoobarService.MakeDuplexStreamingRequest).ConfigureAwait(false); + using var call = client.DuplexStreaming(); } - /// - /// Validates a failed DuplexStreaming call because the client is disposed before completing the RPC. - /// - [Fact] - public void DuplexStreamingDisposed() - { - static void MakeRequest(Foobar.FoobarClient client) + this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); + } + + /// + /// Validates that a downstream interceptor has access to the created Activity + /// and that the caller always sees the correct activity, not our created Activity. + /// + /// A Task. + [Fact] + public async Task DownstreamInterceptorActivityAccess() + { + using var server = FoobarService.Start(); + var channel = new Channel(server.UriString, ChannelCredentials.Insecure); + var callInvoker = channel.CreateCallInvoker(); + + // Activity has a parent + using var parentActivity = new Activity("foo"); + parentActivity.SetIdFormat(ActivityIdFormat.W3C); + parentActivity.Start(); + + // Order of interceptor invocation will be ClientTracingInterceptor -> MetadataInjector + callInvoker = callInvoker.Intercept( + metadata => { - using var call = client.DuplexStreaming(); - } + // This Func is called as part of an internal MetadataInjector interceptor created by gRPC Core. + Assert.True(Activity.Current.Source == GrpcCoreInstrumentation.ActivitySource); + Assert.Equal(parentActivity.Id, Activity.Current.ParentId); + + // Set a tag on the Activity and make sure we can see it afterwardsd + Activity.Current.SetTag("foo", "bar"); + return metadata; + }); + + var interceptorOptions = new ClientTracingInterceptorOptions { ActivityIdentifierValue = Guid.NewGuid() }; + callInvoker = callInvoker.Intercept(new ClientTracingInterceptor(interceptorOptions)); + var client = new Foobar.FoobarClient(callInvoker); - this.TestActivityIsCancelledWhenHandlerDisposed(MakeRequest); + static void ValidateNewTagOnActivity(InterceptorActivityListener listener) + { + var createdActivity = listener.Activity; + Assert.Contains(createdActivity.TagObjects, t => t.Key == "foo" && (string)t.Value == "bar"); } - /// - /// Validates that a downstream interceptor has access to the created Activity - /// and that the caller always sees the correct activity, not our created Activity. - /// - /// A Task. - [Fact] - public async Task DownstreamInterceptorActivityAccess() + // Check the blocking async call + using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) { - using var server = FoobarService.Start(); - var channel = new Channel(server.UriString, ChannelCredentials.Insecure); - var callInvoker = channel.CreateCallInvoker(); + Assert.Equal(parentActivity, Activity.Current); + var response = client.Unary(FoobarService.DefaultRequestMessage); - // Activity has a parent - using var parentActivity = new Activity("foo"); - parentActivity.SetIdFormat(ActivityIdFormat.W3C); - parentActivity.Start(); + Assert.Equal(parentActivity, Activity.Current); - // Order of interceptor invocation will be ClientTracingInterceptor -> MetadataInjector - callInvoker = callInvoker.Intercept( - metadata => - { - // This Func is called as part of an internal MetadataInjector interceptor created by gRPC Core. - Assert.True(Activity.Current.Source == GrpcCoreInstrumentation.ActivitySource); - Assert.Equal(parentActivity.Id, Activity.Current.ParentId); + ValidateNewTagOnActivity(activityListener); + } - // Set a tag on the Activity and make sure we can see it afterwardsd - Activity.Current.SetTag("foo", "bar"); - return metadata; - }); + // Check unary async + using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) + { + Assert.Equal(parentActivity, Activity.Current); + using var call = client.UnaryAsync(FoobarService.DefaultRequestMessage); - var interceptorOptions = new ClientTracingInterceptorOptions { ActivityIdentifierValue = Guid.NewGuid() }; - callInvoker = callInvoker.Intercept(new ClientTracingInterceptor(interceptorOptions)); - var client = new Foobar.FoobarClient(callInvoker); + Assert.Equal(parentActivity, Activity.Current); - static void ValidateNewTagOnActivity(InterceptorActivityListener listener) - { - var createdActivity = listener.Activity; - Assert.Contains(createdActivity.TagObjects, t => t.Key == "foo" && (string)t.Value == "bar"); - } + _ = await call.ResponseAsync.ConfigureAwait(false); - // Check the blocking async call - using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) - { - Assert.Equal(parentActivity, Activity.Current); - var response = client.Unary(FoobarService.DefaultRequestMessage); + Assert.Equal(parentActivity, Activity.Current); - Assert.Equal(parentActivity, Activity.Current); + ValidateNewTagOnActivity(activityListener); + } - ValidateNewTagOnActivity(activityListener); - } + // Check a streaming async call + using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) + { + Assert.Equal(parentActivity, Activity.Current); + using var call = client.DuplexStreaming(); - // Check unary async - using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) - { - Assert.Equal(parentActivity, Activity.Current); - using var call = client.UnaryAsync(FoobarService.DefaultRequestMessage); + Assert.Equal(parentActivity, Activity.Current); - Assert.Equal(parentActivity, Activity.Current); + await call.RequestStream.WriteAsync(FoobarService.DefaultRequestMessage).ConfigureAwait(false); - _ = await call.ResponseAsync.ConfigureAwait(false); + Assert.Equal(parentActivity, Activity.Current); - Assert.Equal(parentActivity, Activity.Current); + await call.RequestStream.CompleteAsync().ConfigureAwait(false); - ValidateNewTagOnActivity(activityListener); - } + Assert.Equal(parentActivity, Activity.Current); - // Check a streaming async call - using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) + while (await call.ResponseStream.MoveNext().ConfigureAwait(false)) { Assert.Equal(parentActivity, Activity.Current); - using var call = client.DuplexStreaming(); - - Assert.Equal(parentActivity, Activity.Current); - - await call.RequestStream.WriteAsync(FoobarService.DefaultRequestMessage).ConfigureAwait(false); + } - Assert.Equal(parentActivity, Activity.Current); + Assert.Equal(parentActivity, Activity.Current); - await call.RequestStream.CompleteAsync().ConfigureAwait(false); + ValidateNewTagOnActivity(activityListener); + } + } - Assert.Equal(parentActivity, Activity.Current); + /// + /// Validates the common activity tags. + /// + /// The activity. + /// The expected status code. + /// if set to true [recorded messages]. + internal static void ValidateCommonActivityTags( + Activity activity, + Grpc.Core.StatusCode expectedStatusCode = Grpc.Core.StatusCode.OK, + bool recordedMessages = false) + { + Assert.NotNull(activity); + Assert.NotNull(activity.Tags); - while (await call.ResponseStream.MoveNext().ConfigureAwait(false)) - { - Assert.Equal(parentActivity, Activity.Current); - } + // The activity was stopped + Assert.True(activity.Duration != default); - Assert.Equal(parentActivity, Activity.Current); + // TagObjects contain non string values + // Tags contains only string values + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcSystem && (string)t.Value == "grpc"); + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcService && (string)t.Value == "OpenTelemetry.Instrumentation.GrpcCore.Test.Foobar"); + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcMethod); + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcGrpcStatusCode && (int)t.Value == (int)expectedStatusCode); - ValidateNewTagOnActivity(activityListener); - } + // Cancelled is not an error. + if (expectedStatusCode != StatusCode.OK && expectedStatusCode != StatusCode.Cancelled) + { + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeOtelStatusCode && (string)t.Value == "ERROR"); } - /// - /// Validates the common activity tags. - /// - /// The activity. - /// The expected status code. - /// if set to true [recorded messages]. - internal static void ValidateCommonActivityTags( - Activity activity, - Grpc.Core.StatusCode expectedStatusCode = Grpc.Core.StatusCode.OK, - bool recordedMessages = false) + if (recordedMessages) { - Assert.NotNull(activity); - Assert.NotNull(activity.Tags); - - // The activity was stopped - Assert.True(activity.Duration != default); - - // TagObjects contain non string values - // Tags contains only string values - Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcSystem && (string)t.Value == "grpc"); - Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcService && (string)t.Value == "OpenTelemetry.Instrumentation.GrpcCore.Test.Foobar"); - Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcMethod); - Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeRpcGrpcStatusCode && (int)t.Value == (int)expectedStatusCode); + // all methods accept a request and return a single response + Assert.NotNull(activity.Events); + var requestMessage = activity.Events.FirstOrDefault(ae => ae.Name == FoobarService.DefaultRequestMessage.GetType().Name); + var responseMessage = activity.Events.FirstOrDefault(ae => ae.Name == FoobarService.DefaultResponseMessage.GetType().Name); - // Cancelled is not an error. - if (expectedStatusCode != StatusCode.OK && expectedStatusCode != StatusCode.Cancelled) + static void ValidateCommonEventAttributes(ActivityEvent activityEvent) { - Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeOtelStatusCode && (string)t.Value == "ERROR"); + Assert.NotNull(activityEvent.Tags); + Assert.Contains(activityEvent.Tags, t => t.Key == "name" && (string)t.Value == "message"); + Assert.Contains(activityEvent.Tags, t => t.Key == SemanticConventions.AttributeMessageID && (int)t.Value == 1); } - if (recordedMessages) - { - // all methods accept a request and return a single response - Assert.NotNull(activity.Events); - var requestMessage = activity.Events.FirstOrDefault(ae => ae.Name == FoobarService.DefaultRequestMessage.GetType().Name); - var responseMessage = activity.Events.FirstOrDefault(ae => ae.Name == FoobarService.DefaultResponseMessage.GetType().Name); - - static void ValidateCommonEventAttributes(ActivityEvent activityEvent) - { - Assert.NotNull(activityEvent.Tags); - Assert.Contains(activityEvent.Tags, t => t.Key == "name" && (string)t.Value == "message"); - Assert.Contains(activityEvent.Tags, t => t.Key == SemanticConventions.AttributeMessageID && (int)t.Value == 1); - } - - Assert.NotEqual(default, requestMessage); - Assert.NotEqual(default, responseMessage); + Assert.NotEqual(default, requestMessage); + Assert.NotEqual(default, responseMessage); - ValidateCommonEventAttributes(requestMessage); - Assert.Contains(requestMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageType && (string)t.Value == "SENT"); - Assert.Contains(requestMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageCompressedSize && (int)t.Value == FoobarService.DefaultRequestMessageSize); + ValidateCommonEventAttributes(requestMessage); + Assert.Contains(requestMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageType && (string)t.Value == "SENT"); + Assert.Contains(requestMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageCompressedSize && (int)t.Value == FoobarService.DefaultRequestMessageSize); - ValidateCommonEventAttributes(responseMessage); - Assert.Contains(responseMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageType && (string)t.Value == "RECEIVED"); - Assert.Contains(requestMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageCompressedSize && (int)t.Value == FoobarService.DefaultResponseMessageSize); - } + ValidateCommonEventAttributes(responseMessage); + Assert.Contains(responseMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageType && (string)t.Value == "RECEIVED"); + Assert.Contains(requestMessage.Tags, t => t.Key == SemanticConventions.AttributeMessageCompressedSize && (int)t.Value == FoobarService.DefaultResponseMessageSize); } + } - /// - /// Tests basic handler success. - /// - /// The client request function. - /// The additional metadata, if any. - /// A Task. - private async Task TestHandlerSuccess(Func clientRequestFunc, Metadata additionalMetadata) - { - var mockPropagator = new Mock(); - PropagationContext capturedPropagationContext = default; - Metadata capturedCarrier = null; - var propagatorCalled = 0; - var originalMetadataCount = additionalMetadata.Count; - - mockPropagator - .Setup( - x => x.Inject( - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .Callback>( - (propagation, carrier, setter) => - { - propagatorCalled++; - capturedPropagationContext = propagation; - capturedCarrier = carrier; - - // Make sure the original metadata make it through - if (additionalMetadata != null) - { - Assert.Equal(capturedCarrier, additionalMetadata); - } - - // Call the actual setter to ensure it updates the carrier. - // It doesn't matter what we put in - setter(capturedCarrier, "bar", "baz"); - }); - - using var server = FoobarService.Start(); - var interceptorOptions = new ClientTracingInterceptorOptions - { - Propagator = mockPropagator.Object, - RecordMessageEvents = true, - ActivityIdentifierValue = Guid.NewGuid(), - }; + /// + /// Tests basic handler success. + /// + /// The client request function. + /// The additional metadata, if any. + /// A Task. + private async Task TestHandlerSuccess(Func clientRequestFunc, Metadata additionalMetadata) + { + var mockPropagator = new Mock(); + PropagationContext capturedPropagationContext = default; + Metadata capturedCarrier = null; + var propagatorCalled = 0; + var originalMetadataCount = additionalMetadata.Count; + + mockPropagator + .Setup( + x => x.Inject( + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback>( + (propagation, carrier, setter) => + { + propagatorCalled++; + capturedPropagationContext = propagation; + capturedCarrier = carrier; - // No Activity parent - using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) - { - var client = FoobarService.ConstructRpcClient(server.UriString, new ClientTracingInterceptor(interceptorOptions)); - await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); + // Make sure the original metadata make it through + if (additionalMetadata != null) + { + Assert.Equal(capturedCarrier, additionalMetadata); + } - Assert.Equal(default, Activity.Current); + // Call the actual setter to ensure it updates the carrier. + // It doesn't matter what we put in + setter(capturedCarrier, "bar", "baz"); + }); - var activity = activityListener.Activity; + using var server = FoobarService.Start(); + var interceptorOptions = new ClientTracingInterceptorOptions + { + Propagator = mockPropagator.Object, + RecordMessageEvents = true, + ActivityIdentifierValue = Guid.NewGuid(), + }; - // Propagator was called exactly once - Assert.Equal(1, propagatorCalled); + // No Activity parent + using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) + { + var client = FoobarService.ConstructRpcClient(server.UriString, new ClientTracingInterceptor(interceptorOptions)); + await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); - // The client tracing interceptor should create a copy of the original call headers before passing to the propagator. - // Retries that sit above this interceptor rely on the original call metadata. - // The propagator should not have mutated the original CallOption headers. - Assert.Equal(originalMetadataCount, additionalMetadata.Count); + Assert.Equal(default, Activity.Current); - // There was no parent activity, so these will be default - Assert.Equal(default, capturedPropagationContext.ActivityContext.TraceId); - Assert.Equal(default, capturedPropagationContext.ActivityContext.SpanId); + var activity = activityListener.Activity; - // Sanity check a valid metadata injection setter. - Assert.NotEmpty(capturedCarrier); + // Propagator was called exactly once + Assert.Equal(1, propagatorCalled); - ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); - Assert.Equal(default, activity.ParentSpanId); - } + // The client tracing interceptor should create a copy of the original call headers before passing to the propagator. + // Retries that sit above this interceptor rely on the original call metadata. + // The propagator should not have mutated the original CallOption headers. + Assert.Equal(originalMetadataCount, additionalMetadata.Count); - propagatorCalled = 0; - capturedPropagationContext = default; + // There was no parent activity, so these will be default + Assert.Equal(default, capturedPropagationContext.ActivityContext.TraceId); + Assert.Equal(default, capturedPropagationContext.ActivityContext.SpanId); - // Activity has a parent - using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) - { - using var parentActivity = new Activity("foo"); - parentActivity.SetIdFormat(ActivityIdFormat.W3C); - parentActivity.Start(); - var client = FoobarService.ConstructRpcClient(server.UriString, new ClientTracingInterceptor(interceptorOptions)); - await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); + // Sanity check a valid metadata injection setter. + Assert.NotEmpty(capturedCarrier); - Assert.Equal(parentActivity, Activity.Current); + ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); + Assert.Equal(default, activity.ParentSpanId); + } - // Propagator was called exactly once - Assert.Equal(1, propagatorCalled); + propagatorCalled = 0; + capturedPropagationContext = default; - // There was a parent activity, so these will have something in them. - Assert.NotEqual(default, capturedPropagationContext.ActivityContext.TraceId); - Assert.NotEqual(default, capturedPropagationContext.ActivityContext.SpanId); + // Activity has a parent + using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) + { + using var parentActivity = new Activity("foo"); + parentActivity.SetIdFormat(ActivityIdFormat.W3C); + parentActivity.Start(); + var client = FoobarService.ConstructRpcClient(server.UriString, new ClientTracingInterceptor(interceptorOptions)); + await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); - var activity = activityListener.Activity; - ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); - Assert.Equal(parentActivity.Id, activity.ParentId); - } - } + Assert.Equal(parentActivity, Activity.Current); - /// - /// Tests basic handler failure. Instructs the server to fail with resources exhausted and validates the created Activity. - /// - /// The client request function. - /// The status code to use for the failure. Defaults to ResourceExhausted. - /// if set to true [validate error description]. - /// An alternate server URI string. - /// - /// A Task. - /// - private async Task TestHandlerFailure( - Func clientRequestFunc, - StatusCode statusCode = StatusCode.ResourceExhausted, - bool validateErrorDescription = true, - string serverUriString = null) - { - using var server = FoobarService.Start(); - var clientInterceptorOptions = new ClientTracingInterceptorOptions { Propagator = new TraceContextPropagator(), ActivityIdentifierValue = Guid.NewGuid() }; - var client = FoobarService.ConstructRpcClient( - serverUriString ?? server.UriString, - new ClientTracingInterceptor(clientInterceptorOptions), - new List - { - new Metadata.Entry(FoobarService.RequestHeaderFailWithStatusCode, statusCode.ToString()), - new Metadata.Entry(FoobarService.RequestHeaderErrorDescription, "fubar"), - }); + // Propagator was called exactly once + Assert.Equal(1, propagatorCalled); - using var activityListener = new InterceptorActivityListener(clientInterceptorOptions.ActivityIdentifierValue); - await Assert.ThrowsAsync(async () => await clientRequestFunc(client, null).ConfigureAwait(false)); + // There was a parent activity, so these will have something in them. + Assert.NotEqual(default, capturedPropagationContext.ActivityContext.TraceId); + Assert.NotEqual(default, capturedPropagationContext.ActivityContext.SpanId); var activity = activityListener.Activity; - ValidateCommonActivityTags(activity, statusCode, false); + ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); + Assert.Equal(parentActivity.Id, activity.ParentId); + } + } - if (validateErrorDescription) + /// + /// Tests basic handler failure. Instructs the server to fail with resources exhausted and validates the created Activity. + /// + /// The client request function. + /// The status code to use for the failure. Defaults to ResourceExhausted. + /// if set to true [validate error description]. + /// An alternate server URI string. + /// + /// A Task. + /// + private async Task TestHandlerFailure( + Func clientRequestFunc, + StatusCode statusCode = StatusCode.ResourceExhausted, + bool validateErrorDescription = true, + string serverUriString = null) + { + using var server = FoobarService.Start(); + var clientInterceptorOptions = new ClientTracingInterceptorOptions { Propagator = new TraceContextPropagator(), ActivityIdentifierValue = Guid.NewGuid() }; + var client = FoobarService.ConstructRpcClient( + serverUriString ?? server.UriString, + new ClientTracingInterceptor(clientInterceptorOptions), + new List { - Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeOtelStatusDescription && ((string)t.Value).Contains("fubar")); - } - } + new Metadata.Entry(FoobarService.RequestHeaderFailWithStatusCode, statusCode.ToString()), + new Metadata.Entry(FoobarService.RequestHeaderErrorDescription, "fubar"), + }); - /// - /// Tests for Activity cancellation when the handler is disposed before completing the RPC. - /// - /// The client request action. - private void TestActivityIsCancelledWhenHandlerDisposed(Action clientRequestAction) - { - using var server = FoobarService.Start(); - var clientInterceptorOptions = new ClientTracingInterceptorOptions { Propagator = new TraceContextPropagator(), ActivityIdentifierValue = Guid.NewGuid() }; - using var activityListener = new InterceptorActivityListener(clientInterceptorOptions.ActivityIdentifierValue); - var client = FoobarService.ConstructRpcClient(server.UriString, new ClientTracingInterceptor(clientInterceptorOptions)); - clientRequestAction(client); + using var activityListener = new InterceptorActivityListener(clientInterceptorOptions.ActivityIdentifierValue); + await Assert.ThrowsAsync(async () => await clientRequestFunc(client, null).ConfigureAwait(false)); - var activity = activityListener.Activity; - ValidateCommonActivityTags(activity, StatusCode.Cancelled, false); + var activity = activityListener.Activity; + ValidateCommonActivityTags(activity, statusCode, false); + + if (validateErrorDescription) + { + Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeOtelStatusDescription && ((string)t.Value).Contains("fubar")); } } + + /// + /// Tests for Activity cancellation when the handler is disposed before completing the RPC. + /// + /// The client request action. + private void TestActivityIsCancelledWhenHandlerDisposed(Action clientRequestAction) + { + using var server = FoobarService.Start(); + var clientInterceptorOptions = new ClientTracingInterceptorOptions { Propagator = new TraceContextPropagator(), ActivityIdentifierValue = Guid.NewGuid() }; + using var activityListener = new InterceptorActivityListener(clientInterceptorOptions.ActivityIdentifierValue); + var client = FoobarService.ConstructRpcClient(server.UriString, new ClientTracingInterceptor(clientInterceptorOptions)); + clientRequestAction(client); + + var activity = activityListener.Activity; + ValidateCommonActivityTags(activity, StatusCode.Cancelled, false); + } } diff --git a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreServerInterceptorTests.cs b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreServerInterceptorTests.cs index 86b0179094f..947f6cc88b9 100644 --- a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreServerInterceptorTests.cs +++ b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/GrpcCoreServerInterceptorTests.cs @@ -21,161 +21,160 @@ using OpenTelemetry.Context.Propagation; using Xunit; -namespace OpenTelemetry.Instrumentation.GrpcCore.Test +namespace OpenTelemetry.Instrumentation.GrpcCore.Test; + +/// +/// Grpc Core server interceptor tests. +/// +public class GrpcCoreServerInterceptorTests { /// - /// Grpc Core server interceptor tests. + /// Validates a successful UnaryServerHandler call. /// - public class GrpcCoreServerInterceptorTests + /// A task. + [Fact] + public async Task UnaryServerHandlerSuccess() { - /// - /// Validates a successful UnaryServerHandler call. - /// - /// A task. - [Fact] - public async Task UnaryServerHandlerSuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeUnaryAsyncRequest).ConfigureAwait(false); - } - - /// - /// Validates a failed UnaryServerHandler call. - /// - /// A task. - [Fact] - public async Task UnaryServerHandlerFail() - { - await this.TestHandlerFailure(FoobarService.MakeUnaryAsyncRequest).ConfigureAwait(false); - } + await this.TestHandlerSuccess(FoobarService.MakeUnaryAsyncRequest).ConfigureAwait(false); + } - /// - /// Validates a successful ClientStreamingServerHandler call. - /// - /// A task. - [Fact] - public async Task ClientStreamingServerHandlerSuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeClientStreamingRequest).ConfigureAwait(false); - } + /// + /// Validates a failed UnaryServerHandler call. + /// + /// A task. + [Fact] + public async Task UnaryServerHandlerFail() + { + await this.TestHandlerFailure(FoobarService.MakeUnaryAsyncRequest).ConfigureAwait(false); + } - /// - /// Validates a failed ClientStreamingServerHandler call. - /// - /// A task. - [Fact] - public async Task ClientStreamingServerHandlerFail() - { - await this.TestHandlerFailure(FoobarService.MakeClientStreamingRequest).ConfigureAwait(false); - } + /// + /// Validates a successful ClientStreamingServerHandler call. + /// + /// A task. + [Fact] + public async Task ClientStreamingServerHandlerSuccess() + { + await this.TestHandlerSuccess(FoobarService.MakeClientStreamingRequest).ConfigureAwait(false); + } - /// - /// Validates a successful ServerStreamingServerHandler call. - /// - /// A task. - [Fact] - public async Task ServerStreamingServerHandlerSuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeServerStreamingRequest).ConfigureAwait(false); - } + /// + /// Validates a failed ClientStreamingServerHandler call. + /// + /// A task. + [Fact] + public async Task ClientStreamingServerHandlerFail() + { + await this.TestHandlerFailure(FoobarService.MakeClientStreamingRequest).ConfigureAwait(false); + } - /// - /// Validates a failed ServerStreamingServerHandler call. - /// - /// A task. - [Fact] - public async Task ServerStreamingServerHandlerFail() - { - await this.TestHandlerFailure(FoobarService.MakeServerStreamingRequest).ConfigureAwait(false); - } + /// + /// Validates a successful ServerStreamingServerHandler call. + /// + /// A task. + [Fact] + public async Task ServerStreamingServerHandlerSuccess() + { + await this.TestHandlerSuccess(FoobarService.MakeServerStreamingRequest).ConfigureAwait(false); + } - /// - /// Validates a successful DuplexStreamingServerHandler call. - /// - /// A task. - [Fact] - public async Task DuplexStreamingServerHandlerSuccess() - { - await this.TestHandlerSuccess(FoobarService.MakeDuplexStreamingRequest).ConfigureAwait(false); - } + /// + /// Validates a failed ServerStreamingServerHandler call. + /// + /// A task. + [Fact] + public async Task ServerStreamingServerHandlerFail() + { + await this.TestHandlerFailure(FoobarService.MakeServerStreamingRequest).ConfigureAwait(false); + } - /// - /// Validates a failed DuplexStreamingServerHandler call. - /// - /// A task. - [Fact] - public async Task DuplexStreamingServerHandlerFail() - { - await this.TestHandlerFailure(FoobarService.MakeDuplexStreamingRequest).ConfigureAwait(false); - } + /// + /// Validates a successful DuplexStreamingServerHandler call. + /// + /// A task. + [Fact] + public async Task DuplexStreamingServerHandlerSuccess() + { + await this.TestHandlerSuccess(FoobarService.MakeDuplexStreamingRequest).ConfigureAwait(false); + } - /// - /// A common method to test server interceptor handler success. - /// - /// The specific client request function. - /// The additional metadata, if any. - /// A Task. - private async Task TestHandlerSuccess(Func clientRequestFunc, Metadata additionalMetadata = null) - { - // starts the server with the server interceptor - var interceptorOptions = new ServerTracingInterceptorOptions { Propagator = new TraceContextPropagator(), RecordMessageEvents = true, ActivityIdentifierValue = Guid.NewGuid() }; - using var server = FoobarService.Start(new ServerTracingInterceptor(interceptorOptions)); + /// + /// Validates a failed DuplexStreamingServerHandler call. + /// + /// A task. + [Fact] + public async Task DuplexStreamingServerHandlerFail() + { + await this.TestHandlerFailure(FoobarService.MakeDuplexStreamingRequest).ConfigureAwait(false); + } - // No parent Activity, no context from header - using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) - { - var client = FoobarService.ConstructRpcClient(server.UriString); - await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); + /// + /// A common method to test server interceptor handler success. + /// + /// The specific client request function. + /// The additional metadata, if any. + /// A Task. + private async Task TestHandlerSuccess(Func clientRequestFunc, Metadata additionalMetadata = null) + { + // starts the server with the server interceptor + var interceptorOptions = new ServerTracingInterceptorOptions { Propagator = new TraceContextPropagator(), RecordMessageEvents = true, ActivityIdentifierValue = Guid.NewGuid() }; + using var server = FoobarService.Start(new ServerTracingInterceptor(interceptorOptions)); - var activity = activityListener.Activity; - GrpcCoreClientInterceptorTests.ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); - Assert.Equal(default, activity.ParentSpanId); - } + // No parent Activity, no context from header + using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) + { + var client = FoobarService.ConstructRpcClient(server.UriString); + await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); - // No parent Activity, context from header - using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) - { - var client = FoobarService.ConstructRpcClient( - server.UriString, - additionalMetadata: new List - { - new Metadata.Entry("traceparent", FoobarService.DefaultTraceparentWithSampling), - }); - - await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); - - var activity = activityListener.Activity; - GrpcCoreClientInterceptorTests.ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); - Assert.Equal(FoobarService.DefaultParentFromTraceparentHeader.SpanId, activity.ParentSpanId); - } + var activity = activityListener.Activity; + GrpcCoreClientInterceptorTests.ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); + Assert.Equal(default, activity.ParentSpanId); } - /// - /// A common method to test server interceptor handler failure. - /// - /// The specific client request function. - /// The additional metadata, if any. - /// A Task. - private async Task TestHandlerFailure(Func clientRequestFunc, Metadata additionalMetadata = null) + // No parent Activity, context from header + using (var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue)) { - // starts the server with the server interceptor - var interceptorOptions = new ServerTracingInterceptorOptions { Propagator = new TraceContextPropagator(), ActivityIdentifierValue = Guid.NewGuid() }; - using var server = FoobarService.Start(new ServerTracingInterceptor(interceptorOptions)); - - using var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue); var client = FoobarService.ConstructRpcClient( server.UriString, additionalMetadata: new List { new Metadata.Entry("traceparent", FoobarService.DefaultTraceparentWithSampling), - new Metadata.Entry(FoobarService.RequestHeaderFailWithStatusCode, StatusCode.ResourceExhausted.ToString()), - new Metadata.Entry(FoobarService.RequestHeaderErrorDescription, "fubar"), }); - await Assert.ThrowsAsync(async () => await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false)); + await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false); var activity = activityListener.Activity; - GrpcCoreClientInterceptorTests.ValidateCommonActivityTags(activity, StatusCode.ResourceExhausted, interceptorOptions.RecordMessageEvents); + GrpcCoreClientInterceptorTests.ValidateCommonActivityTags(activity, StatusCode.OK, interceptorOptions.RecordMessageEvents); Assert.Equal(FoobarService.DefaultParentFromTraceparentHeader.SpanId, activity.ParentSpanId); } } + + /// + /// A common method to test server interceptor handler failure. + /// + /// The specific client request function. + /// The additional metadata, if any. + /// A Task. + private async Task TestHandlerFailure(Func clientRequestFunc, Metadata additionalMetadata = null) + { + // starts the server with the server interceptor + var interceptorOptions = new ServerTracingInterceptorOptions { Propagator = new TraceContextPropagator(), ActivityIdentifierValue = Guid.NewGuid() }; + using var server = FoobarService.Start(new ServerTracingInterceptor(interceptorOptions)); + + using var activityListener = new InterceptorActivityListener(interceptorOptions.ActivityIdentifierValue); + var client = FoobarService.ConstructRpcClient( + server.UriString, + additionalMetadata: new List + { + new Metadata.Entry("traceparent", FoobarService.DefaultTraceparentWithSampling), + new Metadata.Entry(FoobarService.RequestHeaderFailWithStatusCode, StatusCode.ResourceExhausted.ToString()), + new Metadata.Entry(FoobarService.RequestHeaderErrorDescription, "fubar"), + }); + + await Assert.ThrowsAsync(async () => await clientRequestFunc(client, additionalMetadata).ConfigureAwait(false)); + + var activity = activityListener.Activity; + GrpcCoreClientInterceptorTests.ValidateCommonActivityTags(activity, StatusCode.ResourceExhausted, interceptorOptions.RecordMessageEvents); + Assert.Equal(FoobarService.DefaultParentFromTraceparentHeader.SpanId, activity.ParentSpanId); + } } diff --git a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/InterceptorActivityListener.cs b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/InterceptorActivityListener.cs index 2dcfc759e2f..662f013ec57 100644 --- a/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/InterceptorActivityListener.cs +++ b/test/OpenTelemetry.Instrumentation.GrpcCore.Tests/InterceptorActivityListener.cs @@ -18,57 +18,56 @@ using System.Diagnostics; using System.Linq; -namespace OpenTelemetry.Instrumentation.GrpcCore.Test +namespace OpenTelemetry.Instrumentation.GrpcCore.Test; + +/// +/// This class listens for a single Activity created by the Grpc Core interceptors. +/// +internal sealed class InterceptorActivityListener : IDisposable { /// - /// This class listens for a single Activity created by the Grpc Core interceptors. + /// The activity listener. /// - internal sealed class InterceptorActivityListener : IDisposable - { - /// - /// The activity listener. - /// - private readonly ActivityListener activityListener; + private readonly ActivityListener activityListener; - /// - /// Initializes a new instance of the class. - /// - /// The activity identifier. - public InterceptorActivityListener(Guid activityIdentifier) + /// + /// Initializes a new instance of the class. + /// + /// The activity identifier. + public InterceptorActivityListener(Guid activityIdentifier) + { + this.activityListener = new ActivityListener { - this.activityListener = new ActivityListener + ShouldListenTo = source => source.Name == GrpcCoreInstrumentation.ActivitySourceName, + ActivityStarted = activity => { - ShouldListenTo = source => source.Name == GrpcCoreInstrumentation.ActivitySourceName, - ActivityStarted = activity => + if (activity.TagObjects.Any(t => t.Key == SemanticConventions.AttributeActivityIdentifier && (Guid)t.Value == activityIdentifier)) { - if (activity.TagObjects.Any(t => t.Key == SemanticConventions.AttributeActivityIdentifier && (Guid)t.Value == activityIdentifier)) - { - this.Activity = activity; - } - }, - Sample = this.Sample, - }; - - ActivitySource.AddActivityListener(this.activityListener); - Debug.Assert(GrpcCoreInstrumentation.ActivitySource.HasListeners(), "activity source has no listeners"); - } + this.Activity = activity; + } + }, + Sample = this.Sample, + }; - /// - /// Gets the started Activity. - /// - public Activity Activity { get; private set; } + ActivitySource.AddActivityListener(this.activityListener); + Debug.Assert(GrpcCoreInstrumentation.ActivitySource.HasListeners(), "activity source has no listeners"); + } - /// - public void Dispose() - { - this.activityListener.Dispose(); - } + /// + /// Gets the started Activity. + /// + public Activity Activity { get; private set; } - /// - /// Always sample. - /// - /// The options. - /// a result. - private ActivitySamplingResult Sample(ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded; + /// + public void Dispose() + { + this.activityListener.Dispose(); } + + /// + /// Always sample. + /// + /// The options. + /// a result. + private ActivitySamplingResult Sample(ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded; }