From 687df77cf63d50a08018398cb39144c62c607c61 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Tue, 6 Apr 2021 19:56:44 +0900 Subject: [PATCH 01/18] chore: bump to OpenTelemetry 1.0.0-rc1.1 --- .../MagicOnion.Server.OpenTelemetry.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj index 99d15b0eb..2427fc4f3 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -18,8 +18,8 @@ MagicOnion.Server.OpenTelemetry Telemetry Extensions of MagicOnion. $(PackageTags);OpenTelemetry - - beta-080.1 + + rc-100.1.1 From 1d7c053d9d6f323f09ec6ccc1d94c6f56c513a7c Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Tue, 1 Jun 2021 11:56:51 +0900 Subject: [PATCH 02/18] feat: bump OpenTelemetry & penTelemetry.Extensions.Hosting packages --- .../MagicOnion.Server.OpenTelemetry.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj index 2427fc4f3..8dc01a178 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj @@ -26,8 +26,8 @@ - - + + From ec4826c10173db9fc93ee7fa4ee52d2f05d7e201 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:24:50 +0900 Subject: [PATCH 03/18] feat: remove OpenTelemetry Metrics implementations --- .../MagicOnionOpenTelemetryOption.cs | 31 --- .../OpenTelemetryCollectorMeterLogger.cs | 217 ------------------ ...penTelemetryServiceCollectionExtensions.cs | 26 +-- 3 files changed, 3 insertions(+), 271 deletions(-) delete mode 100644 src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorMeterLogger.cs diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs index 3e24877ef..f19342f6d 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs @@ -1,43 +1,12 @@ -using System; using System.Reflection; -using OpenTelemetry.Metrics; -using OpenTelemetry.Metrics.Export; namespace MagicOnion.Server.OpenTelemetry { public class MagicOnionOpenTelemetryOptions { - /// - /// Metrics Exporter Endpoint. Default Prometheus endpoint. - /// - public string MetricsExporterEndpoint { get; set; } = "http://127.0.0.1:9184/metrics/"; - /// - /// Metrics Exporter Hosting Endpoint. - /// - public string MetricsExporterHostingEndpoint { get; set; } = "http://+:9184/metrics/"; /// /// Tracer ServiceName use as ActivitySource /// public string MagicOnionActivityName { get; set; } = Assembly.GetEntryAssembly().GetName().Name; } - - public class MagicOnionOpenTelemetryMeterFactoryOption - { - /// - /// OpenTelemetry MetricsProcessor. default is - /// - public MetricProcessor MetricProcessor { get; set; } = new UngroupedBatcher(); - /// - /// OpenTelemetry MetricsExporter Implementation to use. - /// - public MetricExporter MetricExporter { get; set; } - /// - /// OpenTelemetry Metric Push Interval. - /// - public TimeSpan MetricPushInterval { get; set; } = TimeSpan.FromSeconds(10); - /// - /// MagicOnionLogger to collect OpenTelemetry metrics. - /// - public Func MeterLogger { get; set; } - } } \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorMeterLogger.cs b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorMeterLogger.cs deleted file mode 100644 index 155004acc..000000000 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorMeterLogger.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using Grpc.Core; -using MagicOnion.Server.Hubs; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; - -namespace MagicOnion.Server.OpenTelemetry -{ - /// - /// Collect OpemTelemetry Meter Logger. - /// - public class OpenTelemetryCollectorMeterLogger : IMagicOnionLogger - { - const string MethodKey = "method"; - - readonly IEnumerable> defaultLabels; - readonly ConcurrentDictionary>> labelCache = new ConcurrentDictionary>>(); - readonly ConcurrentDictionary>> broadcastLabelCache = new ConcurrentDictionary>>(); - - readonly MeasureMetric buildServiceDefinitionMeasure; - readonly CounterMetric unaryRequestCounter; - readonly MeasureMetric unaryRequestSizeMeasure; - readonly MeasureMetric unaryResponseSizeMeasure; - readonly CounterMetric unaryErrorCounter; - readonly MeasureMetric unaryElapsedMeasure; - - readonly CounterMetric streamingHubErrorCounter; - readonly MeasureMetric streamingHubElapsedMeasure; - readonly CounterMetric streamingHubRequestCounter; - readonly MeasureMetric streamingHubRequestSizeMeasure; - readonly MeasureMetric streamingHubResponseSizeMeasure; - readonly CounterMetric streamingHubConnectCounter; - readonly CounterMetric streamingHubDisconnectCounter; - - readonly CounterMetric broadcastRequestCounter; - readonly MeasureMetric broadcastRequestSizeMeasure; - readonly CounterMetric broadcastGroupCounter; - - public OpenTelemetryCollectorMeterLogger(MeterProvider meterProvider, string meterName = "MagicOnion", string metricsPrefix = "magiconion", string version = null, IEnumerable> defaultLabels = null) - { - if (meterProvider == null) throw new ArgumentNullException(nameof(meterProvider)); - - // configure defaultTags included as default tag - this.defaultLabels = defaultLabels ?? Array.Empty>(); - - // todo: how to description? - var meter = meterProvider.GetMeter(meterName, version); - - // Service build time. ms - buildServiceDefinitionMeasure = meter.CreateDoubleMeasure($"{metricsPrefix}_buildservicedefinition_duration_milliseconds"); // sum - - // Unary request count. num - unaryRequestCounter = meter.CreateInt64Counter($"{metricsPrefix}_unary_requests_count"); // sum - // Unary API request size. bytes - unaryRequestSizeMeasure = meter.CreateInt64Measure($"{metricsPrefix}_unary_request_size"); // sum - // Unary API response size. bytes - unaryResponseSizeMeasure = meter.CreateInt64Measure($"{metricsPrefix}_unary_response_size"); // sum - // Unary API error Count. num - unaryErrorCounter = meter.CreateInt64Counter($"{metricsPrefix}_unary_error_count"); // sum - // Unary API elapsed time. ms - unaryElapsedMeasure = meter.CreateDoubleMeasure($"{metricsPrefix}_unary_elapsed_milliseconds"); // sum - - // StreamingHub error Count. num - streamingHubErrorCounter = meter.CreateInt64Counter($"{metricsPrefix}_streaminghub_error_count"); // sum - // StreamingHub elapsed time. ms - streamingHubElapsedMeasure = meter.CreateDoubleMeasure($"{metricsPrefix}_streaminghub_elapsed_milliseconds"); // sum - // StreamingHub request count. num - streamingHubRequestCounter = meter.CreateInt64Counter($"{metricsPrefix}_streaminghub_requests_count"); // sum - // StreamingHub request size. bytes - streamingHubRequestSizeMeasure = meter.CreateInt64Measure($"{metricsPrefix}_streaminghub_request_size"); // sum - // StreamingHub response size. bytes - streamingHubResponseSizeMeasure = meter.CreateInt64Measure($"{metricsPrefix}_streaminghub_response_size"); // sum - // ConnectCount - DisconnectCount = current connect count. (successfully disconnected) - // StreamingHub connect count. num - streamingHubConnectCounter = meter.CreateInt64Counter($"{metricsPrefix}_streaminghub_connect_count"); // sum - // StreamingHub disconnect count. num - streamingHubDisconnectCounter = meter.CreateInt64Counter($"{metricsPrefix}_streaminghub_disconnect_count"); // sum - - // HubBroadcast request count. num - broadcastRequestCounter = meter.CreateInt64Counter($"{metricsPrefix}_broadcast_requests_count"); // sum - // HubBroadcast request size. num - broadcastRequestSizeMeasure = meter.CreateInt64Measure($"{metricsPrefix}_broadcast_request_size"); // sum - // HubBroadcast group count. num - broadcastGroupCounter = meter.CreateInt64Counter($"{metricsPrefix}_broadcast_group_count"); // sum - } - - IEnumerable> CreateLabel(ServiceContext context) - { - // Unary start from /{UnaryInterface}/{Method} - var value = context.CallContext.Method; - var label = labelCache.GetOrAdd(value, new HashSet>(defaultLabels) - { - new KeyValuePair( MethodKey, context.CallContext.Method), - }); - return label; - } - IEnumerable> CreateLabel(StreamingHubContext context) - { - // StreamingHub start from {HubInterface}/{Method} - var value = "/" + context.Path; - var label = labelCache.GetOrAdd(value, new HashSet>(defaultLabels) - { - new KeyValuePair( MethodKey, value), - }); - return label; - } - IEnumerable> CreateLabel(string value) - { - var label = labelCache.GetOrAdd(value, new HashSet>(defaultLabels) - { - new KeyValuePair( MethodKey, value), - }); - return label; - } - IEnumerable> CreateBroadcastLabel(string value) - { - var label = broadcastLabelCache.GetOrAdd(value, new HashSet>(defaultLabels) - { - new KeyValuePair( "GroupName", value), - }); - return label; - } - - public void BeginBuildServiceDefinition() - { - } - - public void EndBuildServiceDefinition(double elapsed) - { - buildServiceDefinitionMeasure.Record(default(SpanContext), elapsed, CreateLabel(nameof(EndBuildServiceDefinition))); - } - - public void BeginInvokeMethod(ServiceContext context, byte[] request, Type type) - { - var spanContext = default(SpanContext); - var label = CreateLabel(context); - if (context.MethodType == MethodType.DuplexStreaming && context.CallContext.Method.EndsWith("/Connect")) - { - streamingHubConnectCounter.Add(spanContext, 1, label); - } - else if (context.MethodType == MethodType.Unary) - { - unaryRequestCounter.Add(spanContext, 1, label); - unaryRequestSizeMeasure.Record(spanContext, request.Length, label); - } - } - - public void EndInvokeMethod(ServiceContext context, byte[] response, Type type, double elapsed, bool isErrorOrInterrupted) - { - var spanContext = default(SpanContext); - var label = CreateLabel(context); - if (context.MethodType == MethodType.DuplexStreaming && context.CallContext.Method.EndsWith("/Connect")) - { - streamingHubDisconnectCounter.Add(spanContext, 1, label); - } - else if (context.MethodType == MethodType.Unary) - { - unaryElapsedMeasure.Record(spanContext, elapsed, label); - unaryResponseSizeMeasure.Record(spanContext, response.LongLength, label); - if (isErrorOrInterrupted) - { - unaryErrorCounter.Add(spanContext, 1, label); - } - } - } - - public void BeginInvokeHubMethod(StreamingHubContext context, ReadOnlyMemory request, Type type) - { - var spanContext = default(SpanContext); - var label = CreateLabel(context); - streamingHubRequestCounter.Add(spanContext, 1, label); - streamingHubRequestSizeMeasure.Record(spanContext, request.Length, label); - } - - public void EndInvokeHubMethod(StreamingHubContext context, int responseSize, Type type, double elapsed, bool isErrorOrInterrupted) - { - var spanContext = default(SpanContext); - var label = CreateLabel(context); - streamingHubElapsedMeasure.Record(spanContext, elapsed, label); - streamingHubResponseSizeMeasure.Record(spanContext, responseSize, label); - if (isErrorOrInterrupted) - { - streamingHubErrorCounter.Add(spanContext, 1, label); - } - } - - public void InvokeHubBroadcast(string groupName, int responseSize, int broadcastGroupCount) - { - var spanContext = default(SpanContext); - broadcastRequestCounter.Add(spanContext, 1, CreateBroadcastLabel(groupName)); - broadcastGroupCounter.Add(spanContext, broadcastGroupCount, CreateBroadcastLabel(groupName)); - broadcastRequestSizeMeasure.Record(spanContext, responseSize, CreateBroadcastLabel(groupName)); - } - - public void ReadFromStream(ServiceContext context, byte[] readData, Type type, bool complete) - { - // todo: read stream count - } - - public void WriteToStream(ServiceContext context, byte[] writeData, Type type) - { - // todo: write stream count - } - - public void Error(Exception ex, ServerCallContext context) - { - // todo: exception count - } - - public void Error(Exception ex, StreamingHubContext context) - { - // todo: exception count - } - } -} \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs index 13cf5c4e1..312288f85 100644 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry; -using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using System.Diagnostics; @@ -22,45 +21,26 @@ public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBu public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options) { - return AddOpenTelemetry(builder, options, null, null); + return AddOpenTelemetry(builder, options, null); } /// add MagicOnion Telemetry. public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, - Action configureMeterFactory, Action configureTracerFactory, string configurationName = "") { var options = BindMagicOnionOpenTelemetryOptions(builder, configurationName); - return AddOpenTelemetry(builder, options, configureMeterFactory, configureTracerFactory); + return AddOpenTelemetry(builder, options, configureTracerFactory); } /// add MagicOnion Telemetry. public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options, - Action configureMeterProvider, Action configureTracerProvider) { if (options == null) throw new ArgumentNullException(nameof(options)); builder.Services.AddSingleton(options); - // configure MeterFactory - if (configureMeterProvider != null) - { - var meterFactoryOption = new MagicOnionOpenTelemetryMeterFactoryOption(); - configureMeterProvider(options, meterFactoryOption); - - MeterProvider.SetDefault(Sdk.CreateMeterProviderBuilder() - .SetProcessor(meterFactoryOption.MetricProcessor) - .SetExporter(meterFactoryOption.MetricExporter) - .SetPushInterval(meterFactoryOption.MetricPushInterval) - .Build()); - - builder.Services.AddSingleton(meterFactoryOption.MetricExporter); - if (meterFactoryOption.MeterLogger != null) - { - builder.Services.AddSingleton(meterFactoryOption.MeterLogger.Invoke(MeterProvider.Default)); - } - } + var serviceProvider = builder.Services.BuildServiceProvider(); // configure TracerFactory if (configureTracerProvider != null) From 849cce61104dc4c6f2d5c6d5c66fe273fc41b364 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Tue, 1 Jun 2021 12:25:21 +0900 Subject: [PATCH 04/18] fix: adapt to new AddOpenTelemetryTracing API --- .../OpenTelemetryServiceCollectionExtensions.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs index 312288f85..fd7fdec95 100644 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs @@ -45,20 +45,17 @@ public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBu // configure TracerFactory if (configureTracerProvider != null) { - if (string.IsNullOrEmpty(options.MagicOnionActivityName)) - { - throw new NullReferenceException(nameof(options.MagicOnionActivityName)); - } + var activityName = options.MagicOnionActivityName ?? throw new ArgumentNullException(nameof(options.MagicOnionActivityName)); - builder.Services.AddOpenTelemetryTracing((provider, builder) => + builder.Services.AddOpenTelemetryTracing(configure => { // ActivitySourceName must match to TracerName. - builder.AddSource(options.MagicOnionActivityName); - configureTracerProvider?.Invoke(options, provider, builder); + configure.AddSource(activityName); + configureTracerProvider?.Invoke(options, serviceProvider, configure); }); // Avoid directly register ActivitySource to Singleton for easier identification. - var activitySource = new ActivitySource(options.MagicOnionActivityName); + var activitySource = new ActivitySource(activityName); var magicOnionActivitySources = new MagicOnionActivitySources(activitySource); builder.Services.AddSingleton(magicOnionActivitySources); } From f4e9b7834ddfab264e9e505bef85b297d2b4da9a Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Tue, 1 Jun 2021 18:55:58 +0900 Subject: [PATCH 05/18] feat: move grpc trace key to rpc.grpc. as official do --- ...elemetryCollectorTracerFilterAttributes.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs index 685a94b07..73c2627e7 100644 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs +++ b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs @@ -54,13 +54,14 @@ public override async ValueTask Invoke(ServiceContext context, Func Date: Tue, 1 Jun 2021 20:38:27 +0900 Subject: [PATCH 06/18] feat: change ActivityName from AppName to PackageName. Support app tags. --- .../MagicOnionActivitySources.cs | 1 + .../MagicOnionInstrumentation.cs | 23 ++++++++++++ .../MagicOnionOpenTelemetryOption.cs | 10 +++-- ...elemetryCollectorTracerFilterAttributes.cs | 37 +++++++++++++++---- ...penTelemetryServiceCollectionExtensions.cs | 36 ++++++++++-------- .../ServiceContextTelemetryExtensions.cs | 16 ++++++-- 6 files changed, 94 insertions(+), 29 deletions(-) create mode 100644 src/MagicOnion.Server.OpenTelemetry/MagicOnionInstrumentation.cs diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionActivitySources.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionActivitySources.cs index 9a58ae946..4cbf457c5 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionActivitySources.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionActivitySources.cs @@ -7,6 +7,7 @@ namespace MagicOnion.Server.OpenTelemetry /// /// ActivitySource for MagicOnion OpenTelemetry /// + /// Avoid directly register ActivitySource to Singleton for easier identification. public class MagicOnionActivitySources { private readonly ActivitySource activitySource; diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionInstrumentation.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionInstrumentation.cs new file mode 100644 index 000000000..6a3a624c6 --- /dev/null +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionInstrumentation.cs @@ -0,0 +1,23 @@ +using System; +using System.Reflection; + +namespace MagicOnion.Server.OpenTelemetry +{ + internal static class MagicOnionInstrumentation + { + /// + /// The assembly name. + /// + internal static readonly AssemblyName AssemblyName = typeof(MagicOnionInstrumentation).Assembly.GetName(); + + /// + /// The activity source name. + /// + internal static readonly string ActivitySourceName = AssemblyName.Name; + + /// + /// The version. + /// + internal static readonly Version Version = AssemblyName.Version; + } +} diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs index f19342f6d..807b9252d 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs @@ -1,12 +1,16 @@ -using System.Reflection; +using System; +using System.Collections.Generic; namespace MagicOnion.Server.OpenTelemetry { + /// + /// OpenTelemetry Options to inject Application Information + /// public class MagicOnionOpenTelemetryOptions { /// - /// Tracer ServiceName use as ActivitySource + /// Application specific OpenTelemetry Tracing tags /// - public string MagicOnionActivityName { get; set; } = Assembly.GetEntryAssembly().GetName().Name; + public Dictionary TracingTags { get; set; } = new Dictionary(); } } \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs index 73c2627e7..a4e5576ec 100644 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs +++ b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs @@ -26,16 +26,23 @@ MagicOnionFilterAttribute IMagicOnionFilterFactory.Cr internal class OpenTelemetryCollectorTracerFilterAttribute : MagicOnionFilterAttribute { readonly ActivitySource source; - readonly MagicOnionOpenTelemetryOptions telemetryOption; + readonly MagicOnionOpenTelemetryOptions options; public OpenTelemetryCollectorTracerFilterAttribute(ActivitySource activitySource, MagicOnionOpenTelemetryOptions telemetryOption) { this.source = activitySource; - this.telemetryOption = telemetryOption; + this.options = telemetryOption; } public override async ValueTask Invoke(ServiceContext context, Func next) { + // short-circuit current activity + if (!source.HasListeners()) + { + await next(context); + return; + } + // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc // span name must be `$package.$service/$method` but MagicOnion has no $package. @@ -48,11 +55,15 @@ public override async ValueTask Invoke(ServiceContext context, Func next) { - // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc + // short-circuit current activity + if (!source.HasListeners()) + { + await next(context); + return; + } + // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc using var activity = source.StartActivity($"{context.ServiceContext.MethodType}:/{context.Path}", ActivityKind.Server); // activity may be null if "no one is listening" or "all listener returns ActivitySamplingResult.None in Sample or SampleUsingParentId callback". @@ -126,11 +144,15 @@ public override async ValueTask Invoke(StreamingHubContext context, FuncMagicOnion extensions for Microsoft.Extensions.Hosting classes public static class OpenTelemetryServiceCollectionExtensions { - /// add MagicOnion Telemetry. + /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, string configurationName = "") { var options = BindMagicOnionOpenTelemetryOptions(builder, configurationName); return AddOpenTelemetry(builder, options); } - /// add MagicOnion Telemetry. + /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options) { return AddOpenTelemetry(builder, options, null); } - /// add MagicOnion Telemetry. + /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, Action configureTracerFactory, string configurationName = "") @@ -31,38 +30,45 @@ public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBu var options = BindMagicOnionOpenTelemetryOptions(builder, configurationName); return AddOpenTelemetry(builder, options, configureTracerFactory); } - /// add MagicOnion Telemetry. + /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options, Action configureTracerProvider) { if (options == null) throw new ArgumentNullException(nameof(options)); - - builder.Services.AddSingleton(options); + if (builder == null) throw new ArgumentNullException(nameof(builder)); var serviceProvider = builder.Services.BuildServiceProvider(); + var activityName = MagicOnionInstrumentation.ActivitySourceName; - // configure TracerFactory + + // Configure OpenTelemetry Tracer if (configureTracerProvider != null) { - var activityName = options.MagicOnionActivityName ?? throw new ArgumentNullException(nameof(options.MagicOnionActivityName)); - builder.Services.AddOpenTelemetryTracing(configure => { // ActivitySourceName must match to TracerName. configure.AddSource(activityName); configureTracerProvider?.Invoke(options, serviceProvider, configure); }); - - // Avoid directly register ActivitySource to Singleton for easier identification. - var activitySource = new ActivitySource(activityName); - var magicOnionActivitySources = new MagicOnionActivitySources(activitySource); - builder.Services.AddSingleton(magicOnionActivitySources); } + // auto listen ActivitySource + var activitySource = new ActivitySource(activityName, MagicOnionInstrumentation.Version.ToString()); + + // DI + builder.Services.AddSingleton(options); + builder.Services.AddSingleton(new MagicOnionActivitySources(activitySource)); + return builder; } + /// + /// Generate MagicOnionTelemetryOptions and configure if configuration exists. + /// + /// + /// + /// private static MagicOnionOpenTelemetryOptions BindMagicOnionOpenTelemetryOptions(IMagicOnionServerBuilder builder, string configurationName) { var name = !string.IsNullOrEmpty(configurationName) ? configurationName : "MagicOnion:OpenTelemetry"; diff --git a/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs index 5611b0ae2..50b088c36 100644 --- a/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs @@ -9,7 +9,7 @@ namespace MagicOnion.Server.OpenTelemetry public static class ServiceContextTelemetryExtensions { /// - /// Set the trace context with this service context + /// Set the trace context with this service context. This allows user to add their span directly to this context. /// /// /// @@ -25,14 +25,18 @@ internal static void SetTraceContext(this ServiceContext context, ActivityContex /// public static ActivityContext GetTraceContext(this ServiceContext context) { - return (ActivityContext)context.Items[MagicOnionTelemetry.ServiceContextItemKeyTrace]; + if (context.Items.TryGetValue(MagicOnionTelemetry.ServiceContextItemKeyTrace, out var activityContext)) + { + return (ActivityContext)activityContext; + } + return default; } } public static class StreamingHubContextTelemetryExtensions { /// - /// Set the trace context with this streaming hub context + /// Set the trace context with this streaming hub context. This allows user to add their span directly to this context /// /// /// @@ -48,7 +52,11 @@ internal static void SetTraceContext(this StreamingHubContext context, ActivityC /// public static ActivityContext GetTraceContext(this StreamingHubContext context) { - return (ActivityContext)context.Items[MagicOnionTelemetry.ServiceContextItemKeyTrace]; + if (context.Items.TryGetValue(MagicOnionTelemetry.ServiceContextItemKeyTrace, out var activityContext)) + { + return (ActivityContext)activityContext; + } + return default; } } } \ No newline at end of file From f4b9817a77508c48d87b4ecf916114343fedde3c Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Wed, 2 Jun 2021 17:44:11 +0900 Subject: [PATCH 07/18] feat: add MagicOnionOpenTelemetryClientFilter --- .../MagicOnion.Server.OpenTelemetry.csproj | 1 + .../MagicOnionOpenTelemetryClientFilter.cs | 93 +++++++ ...nionOpenTelemetryTracerFilterAttributes.cs | 243 ++++++++++++++++++ ...s => MagicOnionServerBuilderExtensions.cs} | 2 +- ...elemetryCollectorTracerFilterAttributes.cs | 189 -------------- ...StatusHelper.cs => OpenTelemetryHelper.cs} | 0 .../RpcScope.cs | 112 ++++++++ .../SemanticConventions.cs | 27 ++ .../ServiceContextTelemetryExtensions.cs | 46 +++- 9 files changed, 522 insertions(+), 191 deletions(-) create mode 100644 src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs create mode 100644 src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs rename src/MagicOnion.Server.OpenTelemetry/{OpenTelemetryServiceCollectionExtensions.cs => MagicOnionServerBuilderExtensions.cs} (98%) delete mode 100644 src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs rename src/MagicOnion.Server.OpenTelemetry/{OpenTelemetrygRpcStatusHelper.cs => OpenTelemetryHelper.cs} (100%) create mode 100644 src/MagicOnion.Server.OpenTelemetry/RpcScope.cs create mode 100644 src/MagicOnion.Server.OpenTelemetry/SemanticConventions.cs diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj index 8dc01a178..8071cb36b 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj @@ -31,6 +31,7 @@ + diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs new file mode 100644 index 000000000..e91fafa90 --- /dev/null +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs @@ -0,0 +1,93 @@ +using Grpc.Core; +using MagicOnion.Client; +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; +using System; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace MagicOnion.Server.OpenTelemetry +{ + // note: move package to MagicOnion.Client.OpenTelemetry? + /// + /// Collect OpenTelemetry Tracer with Client filter (Unary). + /// + public class MagicOnionOpenTelemetryClientFilter : IClientFilter + { + readonly ActivitySource source; + readonly MagicOnionOpenTelemetryOptions options; + + public MagicOnionOpenTelemetryClientFilter(ActivitySource activitySource, MagicOnionOpenTelemetryOptions options) + { + this.source = activitySource; + this.options = options; + } + + public async ValueTask SendAsync(RequestContext context, Func> next) + { + using var rpcScope = new ClientRpcScope(context, source); + rpcScope.SetAdditionalTags(options.TracingTags); + + try + { + var response = await next(context); + + rpcScope.Complete(); + return response; + } + catch (Exception ex) + { + rpcScope.CompleteWithException(ex); + throw; + } + finally + { + rpcScope.RestoreParentActivity(); + } + } + } + + internal class ClientRpcScope : RpcScope + { + private readonly RequestContext context; + private readonly ActivitySource source; + + public ClientRpcScope(RequestContext context, ActivitySource source) : base(context.MethodPath, context.MethodPath) + { + this.context = context; + this.source = source; + + // capture the current activity + this.ParentActivity = Activity.Current; + + if (!source.HasListeners()) + return; + + var rpcActivity = source.StartActivity( + context.MethodPath, + ActivityKind.Client, + ParentActivity == default ? default : ParentActivity.Context); + + if (rpcActivity == null) + return; + + var callOptions = context.CallOptions; + if (callOptions.Headers == null) + { + callOptions = callOptions.WithHeaders(new Metadata()); + } + + SetActivity(rpcActivity); + + Propagators.DefaultTextMapPropagator.Inject(new PropagationContext(rpcActivity.Context, Baggage.Current), callOptions); + } + + /// + /// Restores the parent activity. + /// + public void RestoreParentActivity() + { + Activity.Current = this.ParentActivity; + } + } +} diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs new file mode 100644 index 000000000..2e0c07528 --- /dev/null +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Grpc.Core; +using MagicOnion.Server.Hubs; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Trace; + +namespace MagicOnion.Server.OpenTelemetry +{ + /// + /// Collect OpenTelemetry Tracer with Server filter (Unary). + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + public class MagicOnionOpenTelemetryTracerFilterFactoryAttribute : Attribute, IMagicOnionFilterFactory + { + public int Order { get; set; } + + MagicOnionFilterAttribute IMagicOnionFilterFactory.CreateInstance(IServiceProvider serviceProvider) + { + var activitySource = serviceProvider.GetService(); + var options = serviceProvider.GetService(); + return new MagicOnionOpenTelemetryTracerFilterAttribute(activitySource.Current, options); + } + } + + internal class MagicOnionOpenTelemetryTracerFilterAttribute : MagicOnionFilterAttribute + { + readonly ActivitySource source; + readonly MagicOnionOpenTelemetryOptions options; + + public MagicOnionOpenTelemetryTracerFilterAttribute(ActivitySource activitySource, MagicOnionOpenTelemetryOptions telemetryOption) + { + this.source = activitySource; + this.options = telemetryOption; + } + + public override async ValueTask Invoke(ServiceContext context, Func next) + { + // short-circuit current activity + if (!source.HasListeners()) + { + await next(context); + return; + } + + var currentContext = Activity.Current?.Context; + + // Extract the SpanContext, if any from the headers + var metadata = context.CallContext.RequestHeaders; + if (metadata != null) + { + var propagationContext = Propagators.DefaultTextMapPropagator.Extract(currentContext, metadata); + if (propagationContext.ActivityContext.IsValid()) + { + currentContext = propagationContext.ActivityContext; + } + if (propagationContext.Baggage != default) + { + Baggage.Current = propagationContext.Baggage; + } + } + + // span name must be `$package.$service/$method` but MagicOnion has no $package. + using var activity = source.StartActivity($"{context.MethodType}:{context.CallContext.Method}", ActivityKind.Server, currentContext ?? default); + + // activity may be null if "no one is listening" or "all listener returns ActivitySamplingResult.None in Sample or SampleUsingParentId callback". + if (activity == null) + { + await next(context); + return; + } + + // todo: propagate で消せるはず? + // add trace context to service context. + context.SetTraceContext(activity.Context); + + try + { + // tag spec: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc + + // application tags + foreach (var tag in options.TracingTags) + activity.SetTag(tag.Key, tag.Value); + + // request + activity.SetTag(SemanticConventions.AttributeRpcGrpcMethod, context.MethodType.ToString()); + activity.SetTag(SemanticConventions.AttributeRpcSystem, "grpc"); + activity.SetTag(SemanticConventions.AttributeRpcService, context.ServiceType.Name); + activity.SetTag(SemanticConventions.AttributeRpcMethod, context.CallContext.Method); + activity.SetTag(SemanticConventions.AttributeHttpHost, context.CallContext.Host); + activity.SetTag(SemanticConventions.AttributeHttpUrl, context.CallContext.Host + context.CallContext.Method); + activity.SetTag(SemanticConventions.AttributeHttpUserAgent, context.CallContext.RequestHeaders.GetValue("user-agent")); + activity.SetTag(SemanticConventions.AttributeMessageType, "RECIEVED"); + activity.SetTag(SemanticConventions.AttributeMessageId, context.ContextId.ToString()); + activity.SetTag(SemanticConventions.AttributeMessageUncompressedSize, context.GetRawRequest()?.LongLength.ToString() ?? "0"); + + activity.SetTag(SemanticConventions.AttributeMagicOnionPeerName, context.CallContext.Peer); + activity.SetTag(SemanticConventions.AttributeMagicOnionAuthEnabled, (!string.IsNullOrEmpty(context.CallContext.AuthContext.PeerIdentityPropertyName)).ToString()); + activity.SetTag(SemanticConventions.AttributeMagicOnionAuthPeerAuthenticated, context.CallContext.AuthContext.IsPeerAuthenticated.ToString()); + + await next(context); + + // response + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.CallContext.Status.StatusCode).ToString()); + activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(context.CallContext.Status.StatusCode)); + } + catch (Exception ex) + { + activity.SetTag(SemanticConventions.AttributeException, ex.ToString()); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.CallContext.Status.StatusCode).ToString()); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusDetail, context.CallContext.Status.Detail); + activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(Grpc.Core.StatusCode.Internal)); + throw; + } + } + } + + /// + /// Collect OpenTelemetry Tracer with Server filter (StreamingHub). + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] + public class MagicOnionOpenTelemetryStreamingTracerFilterFactoryAttribute : Attribute, IMagicOnionFilterFactory + { + public int Order { get; set; } + + StreamingHubFilterAttribute IMagicOnionFilterFactory.CreateInstance(IServiceProvider serviceProvider) + { + var activitySource = serviceProvider.GetService(); + var options = serviceProvider.GetService(); + return new MagicOnionOpenTelemetryStreamingTracerFilterAttribute(activitySource.Current, options); + } + } + + internal class MagicOnionOpenTelemetryStreamingTracerFilterAttribute : StreamingHubFilterAttribute + { + readonly ActivitySource source; + readonly MagicOnionOpenTelemetryOptions options; + + public MagicOnionOpenTelemetryStreamingTracerFilterAttribute(ActivitySource activitySource, MagicOnionOpenTelemetryOptions telemetryOption) + { + this.source = activitySource; + this.options = telemetryOption; + } + + public override async ValueTask Invoke(StreamingHubContext context, Func next) + { + // short-circuit current activity + if (!source.HasListeners()) + { + await next(context); + return; + } + + var currentContext = Activity.Current?.Context; + + // Extract the SpanContext, if any from the headers + var metadata = context.ServiceContext.CallContext.RequestHeaders; + if (metadata != null) + { + var propagationContext = Propagators.DefaultTextMapPropagator.Extract(currentContext, metadata); + if (propagationContext.ActivityContext.IsValid()) + { + currentContext = propagationContext.ActivityContext; + } + if (propagationContext.Baggage != default) + { + Baggage.Current = propagationContext.Baggage; + } + } + + using var activity = source.StartActivity($"{context.ServiceContext.MethodType}:/{context.Path}", ActivityKind.Server, currentContext ?? default); + + // activity may be null if "no one is listening" or "all listener returns ActivitySamplingResult.None in Sample or SampleUsingParentId callback". + if (activity == null) + { + await next(context); + return; + } + + // todo: propagate で消せるはず? + // add trace context to service context. + context.SetTraceContext(activity.Context); + + try + { + // tag spec: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc + + // application tags + foreach (var tag in options.TracingTags) + activity.SetTag(tag.Key, tag.Value); + + // request + activity.SetTag(SemanticConventions.AttributeRpcGrpcMethod, context.ServiceContext.MethodType.ToString()); + activity.SetTag(SemanticConventions.AttributeRpcSystem, "grpc"); + activity.SetTag(SemanticConventions.AttributeRpcService, context.ServiceContext.ServiceType.Name); + activity.SetTag(SemanticConventions.AttributeRpcMethod, $"/{context.Path}"); + activity.SetTag(SemanticConventions.AttributeHttpHost, context.ServiceContext.CallContext.Host); + activity.SetTag(SemanticConventions.AttributeHttpUrl, context.ServiceContext.CallContext.Host + $"/{context.Path}"); + activity.SetTag(SemanticConventions.AttributeHttpUserAgent, context.ServiceContext.CallContext.RequestHeaders.GetValue("user-agent")); + activity.SetTag(SemanticConventions.AttributeMessageType, "RECIEVED"); + activity.SetTag(SemanticConventions.AttributeMessageId, context.ServiceContext.ContextId.ToString()); + activity.SetTag(SemanticConventions.AttributeMessageUncompressedSize, context.Request.Length.ToString()); + + activity.SetTag(SemanticConventions.AttributeMagicOnionPeerName, context.ServiceContext.CallContext.Peer); + activity.SetTag(SemanticConventions.AttributeMagicOnionAuthEnabled, (!string.IsNullOrEmpty(context.ServiceContext.CallContext.AuthContext.PeerIdentityPropertyName)).ToString()); + activity.SetTag(SemanticConventions.AttributeMagicOnionAuthPeerAuthenticated, context.ServiceContext.CallContext.AuthContext.IsPeerAuthenticated.ToString()); + + await next(context); + + // response + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.ServiceContext.CallContext.Status.StatusCode).ToString()); + activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(context.ServiceContext.CallContext.Status.StatusCode)); + } + catch (Exception ex) + { + activity.SetTag(SemanticConventions.AttributeException, ex.ToString()); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.ServiceContext.CallContext.Status.StatusCode).ToString()); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusDetail, context.ServiceContext.CallContext.Status.Detail); + activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(Grpc.Core.StatusCode.Internal)); + throw; + } + } + + private static readonly Func> MetadataGetter = (metadata, key) => + { + for (var i = 0; i < metadata.Count; i++) + { + var entry = metadata[i]; + if (entry.Key.Equals(key)) + { + return new string[1] { entry.Value }; + } + } + + return Enumerable.Empty(); + }; + } +} \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs similarity index 98% rename from src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs rename to src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs index 4cd82cf10..7b7f6e90f 100644 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryServiceCollectionExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs @@ -7,7 +7,7 @@ namespace MagicOnion.Server.OpenTelemetry { /// MagicOnion extensions for Microsoft.Extensions.Hosting classes - public static class OpenTelemetryServiceCollectionExtensions + public static class MagicOnionServerBuilderExtensions { /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs deleted file mode 100644 index a4e5576ec..000000000 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryCollectorTracerFilterAttributes.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using MagicOnion.Server.Hubs; -using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry.Trace; - -namespace MagicOnion.Server.OpenTelemetry -{ - /// - /// Collect OpenTelemetry Tracer with Unary filter. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - public class OpenTelemetryCollectorTracerFilterFactoryAttribute : Attribute, IMagicOnionFilterFactory - { - public int Order { get; set; } - - MagicOnionFilterAttribute IMagicOnionFilterFactory.CreateInstance(IServiceProvider serviceProvider) - { - var activitySource = serviceProvider.GetService(); - var options = serviceProvider.GetService(); - return new OpenTelemetryCollectorTracerFilterAttribute(activitySource.Current, options); - } - } - - internal class OpenTelemetryCollectorTracerFilterAttribute : MagicOnionFilterAttribute - { - readonly ActivitySource source; - readonly MagicOnionOpenTelemetryOptions options; - - public OpenTelemetryCollectorTracerFilterAttribute(ActivitySource activitySource, MagicOnionOpenTelemetryOptions telemetryOption) - { - this.source = activitySource; - this.options = telemetryOption; - } - - public override async ValueTask Invoke(ServiceContext context, Func next) - { - // short-circuit current activity - if (!source.HasListeners()) - { - await next(context); - return; - } - - // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc - - // span name must be `$package.$service/$method` but MagicOnion has no $package. - using var activity = source.StartActivity($"{context.MethodType}:{context.CallContext.Method}", ActivityKind.Server); - - // activity may be null if "no one is listening" or "all listener returns ActivitySamplingResult.None in Sample or SampleUsingParentId callback". - if (activity == null) - { - await next(context); - return; - } - - // add trace context to service context. - context.SetTraceContext(activity.Context); - - try - { - // application tags - foreach (var tag in options.TracingTags) - activity.SetTag(tag.Key, tag.Value); - - // request - activity.SetTag("rpc.grpc.method", context.MethodType.ToString()); - activity.SetTag("rpc.system", "grpc"); - activity.SetTag("rpc.service", context.ServiceType.Name); - activity.SetTag("rpc.method", context.CallContext.Method); - activity.SetTag("net.peer.name", context.CallContext.Peer); - activity.SetTag("http.host", context.CallContext.Host); - activity.SetTag("http.url", context.CallContext.Host + context.CallContext.Method); - activity.SetTag("http.user_agent", context.CallContext.RequestHeaders.GetValue("user-agent")); - activity.SetTag("message.type", "RECIEVED"); - activity.SetTag("message.id", context.ContextId.ToString()); - activity.SetTag("message.uncompressed_size", context.GetRawRequest()?.LongLength.ToString() ?? "0"); - - activity.SetTag("magiconion.peer.ip", context.CallContext.Peer); - activity.SetTag("magiconion.auth.enabled", (!string.IsNullOrEmpty(context.CallContext.AuthContext.PeerIdentityPropertyName)).ToString()); - activity.SetTag("magiconion.auth.peer.authenticated", context.CallContext.AuthContext.IsPeerAuthenticated.ToString()); - - await next(context); - - // response - activity.SetTag("rpc.grpc.status_code", ((long)context.CallContext.Status.StatusCode).ToString()); - activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(context.CallContext.Status.StatusCode)); - } - catch (Exception ex) - { - activity.SetTag("exception", ex.ToString()); - activity.SetTag("rpc.grpc.status_code", ((long)context.CallContext.Status.StatusCode).ToString()); - activity.SetTag("rpc.grpc.status_detail", context.CallContext.Status.Detail); - activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(Grpc.Core.StatusCode.Internal)); - throw; - } - } - } - - /// - /// Collect OpenTelemetry Tracer with StreamingHub Filter. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - public class OpenTelemetryHubCollectorTracerFilterFactoryAttribute : Attribute, IMagicOnionFilterFactory - { - public int Order { get; set; } - - StreamingHubFilterAttribute IMagicOnionFilterFactory.CreateInstance(IServiceProvider serviceProvider) - { - var activitySource = serviceProvider.GetService(); - var options = serviceProvider.GetService(); - return new OpenTelemetryHubCollectorTracerFilterAttribute(activitySource.Current, options); - } - } - - internal class OpenTelemetryHubCollectorTracerFilterAttribute : StreamingHubFilterAttribute - { - readonly ActivitySource source; - readonly MagicOnionOpenTelemetryOptions options; - - public OpenTelemetryHubCollectorTracerFilterAttribute(ActivitySource activitySource, MagicOnionOpenTelemetryOptions telemetryOption) - { - this.source = activitySource; - this.options = telemetryOption; - } - - public override async ValueTask Invoke(StreamingHubContext context, Func next) - { - // short-circuit current activity - if (!source.HasListeners()) - { - await next(context); - return; - } - - // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc - using var activity = source.StartActivity($"{context.ServiceContext.MethodType}:/{context.Path}", ActivityKind.Server); - - // activity may be null if "no one is listening" or "all listener returns ActivitySamplingResult.None in Sample or SampleUsingParentId callback". - if (activity == null) - { - await next(context); - return; - } - - // add trace context to service context. - context.SetTraceContext(activity.Context); - - try - { - // application tags - foreach (var tag in options.TracingTags) - activity.SetTag(tag.Key, tag.Value); - - // request - activity.SetTag("rpc.grpc.method", context.ServiceContext.MethodType.ToString()); - activity.SetTag("rpc.system", "grpc"); - activity.SetTag("rpc.service", context.ServiceContext.ServiceType.Name); - activity.SetTag("rpc.method", $"/{context.Path}"); - activity.SetTag("net.peer.ip", context.ServiceContext.CallContext.Peer); - activity.SetTag("http.host", context.ServiceContext.CallContext.Host); - activity.SetTag("http.url", context.ServiceContext.CallContext.Host + $"/{context.Path}"); - activity.SetTag("http.user_agent", context.ServiceContext.CallContext.RequestHeaders.GetValue("user-agent")); - activity.SetTag("message.type", "RECIEVED"); - activity.SetTag("message.id", context.ServiceContext.ContextId.ToString()); - activity.SetTag("message.uncompressed_size", context.Request.Length.ToString()); - - activity.SetTag("magiconion.peer.ip", context.ServiceContext.CallContext.Peer); - activity.SetTag("magiconion.auth.enabled", (!string.IsNullOrEmpty(context.ServiceContext.CallContext.AuthContext.PeerIdentityPropertyName)).ToString()); - activity.SetTag("magiconion.auth.peer.authenticated", context.ServiceContext.CallContext.AuthContext.IsPeerAuthenticated.ToString()); - - await next(context); - - // response - activity.SetTag("rpc.grpc.status_code", ((long)context.ServiceContext.CallContext.Status.StatusCode).ToString()); - activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(context.ServiceContext.CallContext.Status.StatusCode)); - } - catch (Exception ex) - { - activity.SetTag("exception", ex.ToString()); - activity.SetTag("rpc.grpc.status_code", ((long)context.ServiceContext.CallContext.Status.StatusCode).ToString()); - activity.SetTag("rpc.grpc.status_detail", context.ServiceContext.CallContext.Status.Detail); - activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(Grpc.Core.StatusCode.Internal)); - throw; - } - } - } -} \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetrygRpcStatusHelper.cs b/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryHelper.cs similarity index 100% rename from src/MagicOnion.Server.OpenTelemetry/OpenTelemetrygRpcStatusHelper.cs rename to src/MagicOnion.Server.OpenTelemetry/OpenTelemetryHelper.cs diff --git a/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs b/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs new file mode 100644 index 000000000..3886442c6 --- /dev/null +++ b/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs @@ -0,0 +1,112 @@ +using Grpc.Core; +using OpenTelemetry.Trace; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace MagicOnion.Server.OpenTelemetry +{ + internal abstract class RpcScope : IDisposable + { + private Activity activity; + private long complete = 0; + + protected string RpcService { get; } + protected string RpcMethod { get; } + protected Activity ParentActivity { get; set; } + + protected RpcScope(string rpcService, string rpcMethod) + { + this.RpcService = rpcService; + this.RpcMethod = rpcMethod; + } + + /// + /// Call or to set activity status. + /// Without complete will records a cancel RPC + /// + public void Dispose() + { + if (activity == null) + { + return; + } + + // If not already completed this will mark the Activity as cancelled. + StopActivity((int)Grpc.Core.StatusCode.Cancelled); + } + + /// + /// Records a complete RPC + /// + public void Complete() + { + if (activity == null) + { + return; + } + + // The overall Span status should remain unset however the grpc status code attribute is required + StopActivity((int)Grpc.Core.StatusCode.OK); + } + + /// + /// Records a failed RPC + /// + /// + public void CompleteWithException(Exception exception) + { + if (this.activity == null) + { + return; + } + + var grpcStatusCode = Grpc.Core.StatusCode.Unknown; + var description = exception.Message; + + if (exception is RpcException rpcException) + { + grpcStatusCode = rpcException.StatusCode; + description = rpcException.Message; + } + + this.StopActivity((int)grpcStatusCode, description); + } + protected void SetActivity(Activity activity) + { + this.activity = activity; + + if (!this.activity.IsAllDataRequested) + { + return; + } + + this.activity.SetTag(SemanticConventions.AttributeRpcSystem, "grpc"); + this.activity.SetTag(SemanticConventions.AttributeRpcService, RpcService); + this.activity.SetTag(SemanticConventions.AttributeRpcMethod, RpcMethod); + } + + public void SetAdditionalTags(IDictionary tags) + { + foreach (var tag in tags) + { + activity.SetTag(tag.Key, tag.Value); + } + } + + private void StopActivity(int statusCode, string statusDescription = null) + { + if (Interlocked.CompareExchange(ref this.complete, 1, 0) == 0) + { + this.activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, statusCode); + if (statusDescription != null) + { + this.activity.SetStatus(global::OpenTelemetry.Trace.Status.Error.WithDescription(statusDescription)); + } + + this.activity.Stop(); + } + } + } +} diff --git a/src/MagicOnion.Server.OpenTelemetry/SemanticConventions.cs b/src/MagicOnion.Server.OpenTelemetry/SemanticConventions.cs new file mode 100644 index 000000000..9586fa412 --- /dev/null +++ b/src/MagicOnion.Server.OpenTelemetry/SemanticConventions.cs @@ -0,0 +1,27 @@ +namespace MagicOnion.Server.OpenTelemetry +{ + internal static class SemanticConventions + { + public const string AttributeException = "exception"; + + public const string AttributeHttpHost = "http.host"; + public const string AttributeHttpUrl = "http.url"; + public const string AttributeHttpUserAgent = "http.user_agent"; + + public const string AttributeRpcGrpcMethod = "rpc.grpc.method"; + public const string AttributeRpcGrpcStatusCode = "rpc.grpc.status_code"; + public const string AttributeRpcGrpcStatusDetail = "rpc.grpc.status_detail"; + public const string AttributeRpcSystem = "rpc.system"; + public const string AttributeRpcService = "rpc.service"; + public const string AttributeRpcMethod = "rpc.method"; + + 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 AttributeMagicOnionPeerName = "magiconion.peer.ip"; + public const string AttributeMagicOnionAuthEnabled = "magiconion.auth.enabled"; + public const string AttributeMagicOnionAuthPeerAuthenticated = "magiconion.auth.peer_authenticated"; + } +} diff --git a/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs index 50b088c36..3de980aef 100644 --- a/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs @@ -1,11 +1,55 @@ -using MagicOnion.Server.OpenTelemetry; using MagicOnion.Server.Hubs; using System.Diagnostics; +using OpenTelemetry.Context.Propagation; +using Grpc.Core; +using System.Collections.Generic; +using System.Linq; +using OpenTelemetry; // ReSharper disable once CheckNamespace namespace MagicOnion.Server.OpenTelemetry { + internal static class PropagatorExtensions + { + /// + /// Injects the context into a carrier + /// + /// + /// + /// + public static void Inject(this TextMapPropagator propagator, PropagationContext context, CallOptions carrier) + { + static void SetMetadata(Metadata metadata, string key, string value) => metadata.Add(new Metadata.Entry(key, value)); + propagator.Inject(context, carrier.Headers, SetMetadata); + } + + /// + /// Extract the context from a carrier + /// + /// + /// + /// + /// + public static PropagationContext Extract(this TextMapPropagator propagator, ActivityContext? activityContext, Metadata carrier) + { + static IEnumerable GetMetadata(Metadata metadata, string key) + { + for (var i = 0; i < metadata.Count; i++) + { + var entry = metadata[i]; + if (entry.Key.Equals(key)) + { + return new string[1] { entry.Value }; + } + } + + return Enumerable.Empty(); + } + return propagator.Extract(new PropagationContext(activityContext ?? default, Baggage.Current), carrier, GetMetadata); + } + } + public static class ServiceContextTelemetryExtensions { /// From cf2ea5ba8350cedf65b2a82774c00503a4c07c09 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Wed, 2 Jun 2021 23:11:55 +0900 Subject: [PATCH 08/18] feat: put IRpsScope to ServiceContext Items --- .../MagicOnionInstrumentation.cs | 2 +- .../MagicOnionTelemetryConstants.cs} | 4 +- .../{ => Internal}/OpenTelemetryHelper.cs | 4 +- .../Internal/PropagatorExtensions.cs | 51 ++++ .../{ => Internal}/SemanticConventions.cs | 7 +- .../MagicOnionOpenTelemetryClientFilter.cs | 18 +- ...nionOpenTelemetryTracerFilterAttributes.cs | 218 +++++++----------- .../MagicOnionServerBuilderExtensions.cs | 1 + .../RpcScope.cs | 36 ++- .../ServiceContextTelemetryExtensions.cs | 91 ++------ 10 files changed, 200 insertions(+), 232 deletions(-) rename src/MagicOnion.Server.OpenTelemetry/{ => Internal}/MagicOnionInstrumentation.cs (92%) rename src/MagicOnion.Server.OpenTelemetry/{MagicOnionTelemetry.cs => Internal/MagicOnionTelemetryConstants.cs} (59%) rename src/MagicOnion.Server.OpenTelemetry/{ => Internal}/OpenTelemetryHelper.cs (80%) create mode 100644 src/MagicOnion.Server.OpenTelemetry/Internal/PropagatorExtensions.cs rename src/MagicOnion.Server.OpenTelemetry/{ => Internal}/SemanticConventions.cs (82%) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionInstrumentation.cs b/src/MagicOnion.Server.OpenTelemetry/Internal/MagicOnionInstrumentation.cs similarity index 92% rename from src/MagicOnion.Server.OpenTelemetry/MagicOnionInstrumentation.cs rename to src/MagicOnion.Server.OpenTelemetry/Internal/MagicOnionInstrumentation.cs index 6a3a624c6..fb0fcb7fc 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionInstrumentation.cs +++ b/src/MagicOnion.Server.OpenTelemetry/Internal/MagicOnionInstrumentation.cs @@ -1,7 +1,7 @@ using System; using System.Reflection; -namespace MagicOnion.Server.OpenTelemetry +namespace MagicOnion.Server.OpenTelemetry.Internal { internal static class MagicOnionInstrumentation { diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionTelemetry.cs b/src/MagicOnion.Server.OpenTelemetry/Internal/MagicOnionTelemetryConstants.cs similarity index 59% rename from src/MagicOnion.Server.OpenTelemetry/MagicOnionTelemetry.cs rename to src/MagicOnion.Server.OpenTelemetry/Internal/MagicOnionTelemetryConstants.cs index 2beec7d7e..013912938 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionTelemetry.cs +++ b/src/MagicOnion.Server.OpenTelemetry/Internal/MagicOnionTelemetryConstants.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Text; -namespace MagicOnion.Server.OpenTelemetry +namespace MagicOnion.Server.OpenTelemetry.Internal { - public static class MagicOnionTelemetry + internal static class MagicOnionTelemetryConstants { public const string ServiceContextItemKeyTrace = ".TraceContext"; } diff --git a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryHelper.cs b/src/MagicOnion.Server.OpenTelemetry/Internal/OpenTelemetryHelper.cs similarity index 80% rename from src/MagicOnion.Server.OpenTelemetry/OpenTelemetryHelper.cs rename to src/MagicOnion.Server.OpenTelemetry/Internal/OpenTelemetryHelper.cs index 7bf78d65b..c1e00117f 100644 --- a/src/MagicOnion.Server.OpenTelemetry/OpenTelemetryHelper.cs +++ b/src/MagicOnion.Server.OpenTelemetry/Internal/OpenTelemetryHelper.cs @@ -1,6 +1,6 @@ using Grpc.Core; -namespace MagicOnion.Server.OpenTelemetry +namespace MagicOnion.Server.OpenTelemetry.Internal { public static class OpenTelemetryHelper { @@ -10,7 +10,7 @@ public static class OpenTelemetryHelper /// spec: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/api.md#set-status /// /// - public static global::OpenTelemetry.Trace.Status GrpcToOpenTelemetryStatus(StatusCode code) + internal static global::OpenTelemetry.Trace.Status GrpcToOpenTelemetryStatus(StatusCode code) { return code switch { diff --git a/src/MagicOnion.Server.OpenTelemetry/Internal/PropagatorExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/Internal/PropagatorExtensions.cs new file mode 100644 index 000000000..cf0aa1d28 --- /dev/null +++ b/src/MagicOnion.Server.OpenTelemetry/Internal/PropagatorExtensions.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; +using OpenTelemetry.Context.Propagation; +using Grpc.Core; +using System.Collections.Generic; +using System.Linq; +using OpenTelemetry; + +// ReSharper disable once CheckNamespace + +namespace MagicOnion.Server.OpenTelemetry.Internal +{ + internal static class PropagatorExtensions + { + /// + /// Injects the context into a carrier + /// + /// + /// + /// + public static void Inject(this TextMapPropagator propagator, PropagationContext context, CallOptions carrier) + { + static void SetMetadata(Metadata metadata, string key, string value) => metadata.Add(new Metadata.Entry(key, value)); + propagator.Inject(context, carrier.Headers, SetMetadata); + } + + /// + /// Extract the context from a carrier + /// + /// + /// + /// + /// + public static PropagationContext Extract(this TextMapPropagator propagator, ActivityContext? activityContext, Metadata carrier) + { + static IEnumerable GetMetadata(Metadata metadata, string key) + { + for (var i = 0; i < metadata.Count; i++) + { + var entry = metadata[i]; + if (entry.Key.Equals(key)) + { + return new string[1] { entry.Value }; + } + } + + return Enumerable.Empty(); + } + return propagator.Extract(new PropagationContext(activityContext ?? default, Baggage.Current), carrier, GetMetadata); + } + } +} \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/SemanticConventions.cs b/src/MagicOnion.Server.OpenTelemetry/Internal/SemanticConventions.cs similarity index 82% rename from src/MagicOnion.Server.OpenTelemetry/SemanticConventions.cs rename to src/MagicOnion.Server.OpenTelemetry/Internal/SemanticConventions.cs index 9586fa412..d84eaf494 100644 --- a/src/MagicOnion.Server.OpenTelemetry/SemanticConventions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/Internal/SemanticConventions.cs @@ -1,7 +1,11 @@ -namespace MagicOnion.Server.OpenTelemetry +namespace MagicOnion.Server.OpenTelemetry.Internal { + /// + /// OpenTelemetry Tag Keys + /// internal static class SemanticConventions { + // tag spec: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc public const string AttributeException = "exception"; public const string AttributeHttpHost = "http.host"; @@ -15,7 +19,6 @@ internal static class SemanticConventions public const string AttributeRpcService = "rpc.service"; public const string AttributeRpcMethod = "rpc.method"; - 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"; diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs index e91fafa90..b9b3e3502 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs @@ -1,5 +1,6 @@ using Grpc.Core; using MagicOnion.Client; +using MagicOnion.Server.OpenTelemetry.Internal; using OpenTelemetry; using OpenTelemetry.Context.Propagation; using System; @@ -25,13 +26,14 @@ public MagicOnionOpenTelemetryClientFilter(ActivitySource activitySource, MagicO public async ValueTask SendAsync(RequestContext context, Func> next) { - using var rpcScope = new ClientRpcScope(context, source); - rpcScope.SetAdditionalTags(options.TracingTags); + var rpcService = context.MethodPath.Split('/')[0]; + using var rpcScope = new ClientRpcScope(rpcService, context.MethodPath, context, source); + rpcScope.SetTags(options.TracingTags); try { var response = await next(context); - + rpcScope.Complete(); return response; } @@ -49,14 +51,8 @@ public async ValueTask SendAsync(RequestContext context, Func next) { - // short-circuit current activity - if (!source.HasListeners()) - { - await next(context); - return; - } - - var currentContext = Activity.Current?.Context; - - // Extract the SpanContext, if any from the headers - var metadata = context.CallContext.RequestHeaders; - if (metadata != null) - { - var propagationContext = Propagators.DefaultTextMapPropagator.Extract(currentContext, metadata); - if (propagationContext.ActivityContext.IsValid()) - { - currentContext = propagationContext.ActivityContext; - } - if (propagationContext.Baggage != default) - { - Baggage.Current = propagationContext.Baggage; - } - } - - // span name must be `$package.$service/$method` but MagicOnion has no $package. - using var activity = source.StartActivity($"{context.MethodType}:{context.CallContext.Method}", ActivityKind.Server, currentContext ?? default); - - // activity may be null if "no one is listening" or "all listener returns ActivitySamplingResult.None in Sample or SampleUsingParentId callback". - if (activity == null) + using var rpcScope = new ServerRpcScope(context.ServiceType.Name, context.CallContext.Method.TrimStart('/'), context.CallContext, source); + context.SetTraceScope(rpcScope); + rpcScope.SetTags(options.TracingTags); + rpcScope.SetTags(new Dictionary { - await next(context); - return; - } - - // todo: propagate で消せるはず? - // add trace context to service context. - context.SetTraceContext(activity.Context); + { SemanticConventions.AttributeRpcGrpcMethod, context.MethodType.ToString() }, + { SemanticConventions.AttributeHttpHost, context.CallContext.Host}, + { SemanticConventions.AttributeHttpUrl, context.CallContext.Host + context.CallContext.Method }, + { SemanticConventions.AttributeHttpUserAgent, context.CallContext.RequestHeaders.GetValue("user-agent")}, + { SemanticConventions.AttributeMessageId, context.ContextId.ToString()}, + { SemanticConventions.AttributeMessageUncompressedSize, context.GetRawRequest()?.LongLength.ToString() ?? "0"}, + { SemanticConventions.AttributeMagicOnionPeerName, context.CallContext.Peer}, + { SemanticConventions.AttributeMagicOnionAuthEnabled, (!string.IsNullOrEmpty(context.CallContext.AuthContext.PeerIdentityPropertyName)).ToString()}, + { SemanticConventions.AttributeMagicOnionAuthPeerAuthenticated, context.CallContext.AuthContext.IsPeerAuthenticated.ToString()}, + }); try { - // tag spec: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc - - // application tags - foreach (var tag in options.TracingTags) - activity.SetTag(tag.Key, tag.Value); - - // request - activity.SetTag(SemanticConventions.AttributeRpcGrpcMethod, context.MethodType.ToString()); - activity.SetTag(SemanticConventions.AttributeRpcSystem, "grpc"); - activity.SetTag(SemanticConventions.AttributeRpcService, context.ServiceType.Name); - activity.SetTag(SemanticConventions.AttributeRpcMethod, context.CallContext.Method); - activity.SetTag(SemanticConventions.AttributeHttpHost, context.CallContext.Host); - activity.SetTag(SemanticConventions.AttributeHttpUrl, context.CallContext.Host + context.CallContext.Method); - activity.SetTag(SemanticConventions.AttributeHttpUserAgent, context.CallContext.RequestHeaders.GetValue("user-agent")); - activity.SetTag(SemanticConventions.AttributeMessageType, "RECIEVED"); - activity.SetTag(SemanticConventions.AttributeMessageId, context.ContextId.ToString()); - activity.SetTag(SemanticConventions.AttributeMessageUncompressedSize, context.GetRawRequest()?.LongLength.ToString() ?? "0"); - - activity.SetTag(SemanticConventions.AttributeMagicOnionPeerName, context.CallContext.Peer); - activity.SetTag(SemanticConventions.AttributeMagicOnionAuthEnabled, (!string.IsNullOrEmpty(context.CallContext.AuthContext.PeerIdentityPropertyName)).ToString()); - activity.SetTag(SemanticConventions.AttributeMagicOnionAuthPeerAuthenticated, context.CallContext.AuthContext.IsPeerAuthenticated.ToString()); - await next(context); - // response - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.CallContext.Status.StatusCode).ToString()); - activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(context.CallContext.Status.StatusCode)); + OpenTelemetryHelper.GrpcToOpenTelemetryStatus(context.CallContext.Status.StatusCode); + rpcScope.Complete(context.CallContext.Status.StatusCode); } catch (Exception ex) { - activity.SetTag(SemanticConventions.AttributeException, ex.ToString()); - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.CallContext.Status.StatusCode).ToString()); - activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusDetail, context.CallContext.Status.Detail); - activity.SetStatus(OpenTelemetryHelper.GrpcToOpenTelemetryStatus(Grpc.Core.StatusCode.Internal)); + rpcScope.SetTags(new Dictionary + { + { SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.CallContext.Status.StatusCode).ToString()}, + { SemanticConventions.AttributeRpcGrpcStatusDetail, context.CallContext.Status.Detail}, + }); + rpcScope.CompleteWithException(ex); throw; } + finally + { + rpcScope.RestoreParentActivity(); + } } } @@ -149,17 +109,59 @@ public MagicOnionOpenTelemetryStreamingTracerFilterAttribute(ActivitySource acti public override async ValueTask Invoke(StreamingHubContext context, Func next) { - // short-circuit current activity - if (!source.HasListeners()) + using var rpcScope = new ServerRpcScope(context.ServiceContext.ServiceType.Name, context.Path, context.ServiceContext.CallContext, source); + context.SetTraceScope(rpcScope); + rpcScope.SetTags(options.TracingTags); + rpcScope.SetTags(new Dictionary + { + { SemanticConventions.AttributeRpcGrpcMethod, context.ServiceContext.MethodType.ToString() }, + { SemanticConventions.AttributeHttpHost, context.ServiceContext.CallContext.Host}, + { SemanticConventions.AttributeHttpUrl, context.ServiceContext.CallContext.Host + "/" + context.Path }, + { SemanticConventions.AttributeHttpUserAgent, context.ServiceContext.CallContext.RequestHeaders.GetValue("user-agent")}, + { SemanticConventions.AttributeMessageId, context.ServiceContext.ContextId.ToString()}, + { SemanticConventions.AttributeMessageUncompressedSize, context.Request.Length.ToString()}, + { SemanticConventions.AttributeMagicOnionPeerName, context.ServiceContext.CallContext.Peer}, + { SemanticConventions.AttributeMagicOnionAuthEnabled, (!string.IsNullOrEmpty(context.ServiceContext.CallContext.AuthContext.PeerIdentityPropertyName)).ToString()}, + { SemanticConventions.AttributeMagicOnionAuthPeerAuthenticated, context.ServiceContext.CallContext.AuthContext.IsPeerAuthenticated.ToString()}, + }); + + try { await next(context); - return; + + OpenTelemetryHelper.GrpcToOpenTelemetryStatus(context.ServiceContext.CallContext.Status.StatusCode); + rpcScope.Complete(context.ServiceContext.CallContext.Status.StatusCode); } + catch (Exception ex) + { + rpcScope.SetTags(new Dictionary + { + { SemanticConventions.AttributeRpcGrpcStatusCode, ((long)context.ServiceContext.CallContext.Status.StatusCode).ToString()}, + { SemanticConventions.AttributeRpcGrpcStatusDetail, context.ServiceContext.CallContext.Status.Detail}, + }); + rpcScope.CompleteWithException(ex); + throw; + } + finally + { + rpcScope.RestoreParentActivity(); + } + } + } + + internal class ServerRpcScope : RpcScope + { + + public ServerRpcScope(string rpcService, string rpcMethod, ServerCallContext context, ActivitySource source) : base(rpcService, rpcMethod) + { + // activity may be null if "no one is listening" or "all listener returns ActivitySamplingResult.None in Sample or SampleUsingParentId callback". + if (!source.HasListeners()) + return; var currentContext = Activity.Current?.Context; // Extract the SpanContext, if any from the headers - var metadata = context.ServiceContext.CallContext.RequestHeaders; + var metadata = context.RequestHeaders; if (metadata != null) { var propagationContext = Propagators.DefaultTextMapPropagator.Extract(currentContext, metadata); @@ -173,71 +175,21 @@ public override async ValueTask Invoke(StreamingHubContext context, Func> MetadataGetter = (metadata, key) => + /// + /// Restores the parent activity. + /// + public void RestoreParentActivity() { - for (var i = 0; i < metadata.Count; i++) - { - var entry = metadata[i]; - if (entry.Key.Equals(key)) - { - return new string[1] { entry.Value }; - } - } - - return Enumerable.Empty(); - }; + Activity.Current = this.ParentActivity; + } } } \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs index 7b7f6e90f..f5e55c086 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Trace; using System.Diagnostics; +using MagicOnion.Server.OpenTelemetry.Internal; namespace MagicOnion.Server.OpenTelemetry { diff --git a/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs b/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs index 3886442c6..e83045195 100644 --- a/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs +++ b/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs @@ -1,4 +1,5 @@ using Grpc.Core; +using MagicOnion.Server.OpenTelemetry.Internal; using OpenTelemetry.Trace; using System; using System.Collections.Generic; @@ -7,7 +8,18 @@ namespace MagicOnion.Server.OpenTelemetry { - internal abstract class RpcScope : IDisposable + /// + /// Manage trace activity scope for this Rpc + /// + public interface IRpcScope + { + /// + /// Set custom tag to the activity. + /// + /// + void SetTags(IDictionary tags); + } + internal abstract class RpcScope : IDisposable, IRpcScope { private Activity activity; private long complete = 0; @@ -18,8 +30,8 @@ internal abstract class RpcScope : IDisposable protected RpcScope(string rpcService, string rpcMethod) { - this.RpcService = rpcService; - this.RpcMethod = rpcMethod; + RpcService = rpcService; + RpcMethod = rpcMethod; } /// @@ -37,10 +49,11 @@ public void Dispose() StopActivity((int)Grpc.Core.StatusCode.Cancelled); } + /// /// Records a complete RPC /// - public void Complete() + public void Complete(Grpc.Core.StatusCode statusCode = Grpc.Core.StatusCode.OK) { if (activity == null) { @@ -48,7 +61,7 @@ public void Complete() } // The overall Span status should remain unset however the grpc status code attribute is required - StopActivity((int)Grpc.Core.StatusCode.OK); + StopActivity((int)statusCode); } /// @@ -57,7 +70,7 @@ public void Complete() /// public void CompleteWithException(Exception exception) { - if (this.activity == null) + if (activity == null) { return; } @@ -71,7 +84,8 @@ public void CompleteWithException(Exception exception) description = rpcException.Message; } - this.StopActivity((int)grpcStatusCode, description); + activity.SetTag(SemanticConventions.AttributeException, exception.ToString()); + StopActivity((int)grpcStatusCode, description); } protected void SetActivity(Activity activity) { @@ -87,7 +101,7 @@ protected void SetActivity(Activity activity) this.activity.SetTag(SemanticConventions.AttributeRpcMethod, RpcMethod); } - public void SetAdditionalTags(IDictionary tags) + public void SetTags(IDictionary tags) { foreach (var tag in tags) { @@ -99,13 +113,13 @@ private void StopActivity(int statusCode, string statusDescription = null) { if (Interlocked.CompareExchange(ref this.complete, 1, 0) == 0) { - this.activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, statusCode); + activity.SetTag(SemanticConventions.AttributeRpcGrpcStatusCode, statusCode); if (statusDescription != null) { - this.activity.SetStatus(global::OpenTelemetry.Trace.Status.Error.WithDescription(statusDescription)); + activity.SetStatus(global::OpenTelemetry.Trace.Status.Error.WithDescription(statusDescription)); } - this.activity.Stop(); + activity.Stop(); } } } diff --git a/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs index 3de980aef..f1ef9d806 100644 --- a/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/ServiceContextTelemetryExtensions.cs @@ -1,106 +1,57 @@ using MagicOnion.Server.Hubs; -using System.Diagnostics; -using OpenTelemetry.Context.Propagation; -using Grpc.Core; -using System.Collections.Generic; -using System.Linq; -using OpenTelemetry; - -// ReSharper disable once CheckNamespace +using MagicOnion.Server.OpenTelemetry.Internal; namespace MagicOnion.Server.OpenTelemetry { - internal static class PropagatorExtensions + public static class ServiceContextTelemetryExtensions { /// - /// Injects the context into a carrier + /// Set the trace scope with this service context. /// - /// /// - /// - public static void Inject(this TextMapPropagator propagator, PropagationContext context, CallOptions carrier) + /// + internal static void SetTraceScope(this StreamingHubContext context, IRpcScope scope) { - static void SetMetadata(Metadata metadata, string key, string value) => metadata.Add(new Metadata.Entry(key, value)); - propagator.Inject(context, carrier.Headers, SetMetadata); + context.ServiceContext.Items[MagicOnionTelemetryConstants.ServiceContextItemKeyTrace + "." + context.Path] = scope; } /// - /// Extract the context from a carrier - /// - /// - /// - /// - /// - public static PropagationContext Extract(this TextMapPropagator propagator, ActivityContext? activityContext, Metadata carrier) - { - static IEnumerable GetMetadata(Metadata metadata, string key) - { - for (var i = 0; i < metadata.Count; i++) - { - var entry = metadata[i]; - if (entry.Key.Equals(key)) - { - return new string[1] { entry.Value }; - } - } - - return Enumerable.Empty(); - } - return propagator.Extract(new PropagationContext(activityContext ?? default, Baggage.Current), carrier, GetMetadata); - } - } - - public static class ServiceContextTelemetryExtensions - { - /// - /// Set the trace context with this service context. This allows user to add their span directly to this context. + /// Set the trace scope with this service context. This allows user to add their tag directly to this activity. /// /// - /// - internal static void SetTraceContext(this ServiceContext context, ActivityContext activityContext) + /// + internal static void SetTraceScope(this ServiceContext context, IRpcScope scope) { - context.Items[MagicOnionTelemetry.ServiceContextItemKeyTrace] = activityContext; + context.Items[MagicOnionTelemetryConstants.ServiceContextItemKeyTrace] = scope; } /// - /// Gets the trace context associated with this service context. + /// Gets the trace scope associated with this service context. /// /// /// - public static ActivityContext GetTraceContext(this ServiceContext context) + public static IRpcScope GetTraceScope(this ServiceContext context) { - if (context.Items.TryGetValue(MagicOnionTelemetry.ServiceContextItemKeyTrace, out var activityContext)) + if (context.Items.TryGetValue(MagicOnionTelemetryConstants.ServiceContextItemKeyTrace, out var scope)) { - return (ActivityContext)activityContext; + return (IRpcScope)scope; } return default; } - } - - public static class StreamingHubContextTelemetryExtensions - { - /// - /// Set the trace context with this streaming hub context. This allows user to add their span directly to this context - /// - /// - /// - internal static void SetTraceContext(this StreamingHubContext context, ActivityContext activityContext) - { - context.Items[MagicOnionTelemetry.ServiceContextItemKeyTrace] = activityContext; - } - /// - /// Gets the trace context associated with this streaming hub context. + /// Gets the trace scope associated with this service context. /// + /// Add custom tag directly to this activity. /// + /// IHubClass/MethodName /// - public static ActivityContext GetTraceContext(this StreamingHubContext context) + public static IRpcScope GetTraceScope(this ServiceContext context, string hubPath) { - if (context.Items.TryGetValue(MagicOnionTelemetry.ServiceContextItemKeyTrace, out var activityContext)) + if (context.Items.TryGetValue(MagicOnionTelemetryConstants.ServiceContextItemKeyTrace + "." + hubPath, out var scope)) { - return (ActivityContext)activityContext; + return (IRpcScope)scope; } return default; } } -} \ No newline at end of file +} From 42cc72ffbaa7c2a86e43f8ce86816aefd678e3c1 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Wed, 2 Jun 2021 23:12:12 +0900 Subject: [PATCH 09/18] feat: add option to handle expose IRpsScope in ServiceContext.Items --- .../MagicOnionOpenTelemetryOption.cs | 5 +++++ ...agicOnionOpenTelemetryTracerFilterAttributes.cs | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs index 807b9252d..16462c263 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryOption.cs @@ -12,5 +12,10 @@ public class MagicOnionOpenTelemetryOptions /// Application specific OpenTelemetry Tracing tags /// public Dictionary TracingTags { get; set; } = new Dictionary(); + + /// + /// Expose RpsScope to the ServiceContext.Items. RpsScope key begin with .TraceContext + /// + public bool ExposeRpcScope { get; set; } = true; } } \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs index 8ceb62a6e..9cfd1dd65 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs @@ -41,7 +41,12 @@ public MagicOnionOpenTelemetryTracerFilterAttribute(ActivitySource activitySourc public override async ValueTask Invoke(ServiceContext context, Func next) { using var rpcScope = new ServerRpcScope(context.ServiceType.Name, context.CallContext.Method.TrimStart('/'), context.CallContext, source); - context.SetTraceScope(rpcScope); + + if (options.ExposeRpcScope) + { + context.SetTraceScope(rpcScope); + } + rpcScope.SetTags(options.TracingTags); rpcScope.SetTags(new Dictionary { @@ -110,7 +115,12 @@ public MagicOnionOpenTelemetryStreamingTracerFilterAttribute(ActivitySource acti public override async ValueTask Invoke(StreamingHubContext context, Func next) { using var rpcScope = new ServerRpcScope(context.ServiceContext.ServiceType.Name, context.Path, context.ServiceContext.CallContext, source); - context.SetTraceScope(rpcScope); + + if (options.ExposeRpcScope) + { + context.SetTraceScope(rpcScope); + } + rpcScope.SetTags(options.TracingTags); rpcScope.SetTags(new Dictionary { From becd58e20a26436d2e4036d75952ab00f769db9e Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Wed, 2 Jun 2021 23:15:25 +0900 Subject: [PATCH 10/18] chore: change suffix version to match 1.0.0-rc4 --- .../MagicOnion.Server.OpenTelemetry.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj index 8071cb36b..b2a352dd2 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj @@ -18,8 +18,8 @@ MagicOnion.Server.OpenTelemetry Telemetry Extensions of MagicOnion. $(PackageTags);OpenTelemetry - - rc-100.1.1 + + rc4-1.0.0 From 3c7b4fc52519f7c5a55364baee187a60a3cd1615 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Thu, 3 Jun 2021 02:11:35 +0900 Subject: [PATCH 11/18] chore: ChatApp.Telemetry docker refresh --- README.md | 2 +- .../Internal/OpenTelemetryHelper.cs | 2 +- .../Internal/SemanticConventions.cs | 1 + .../MagicOnionOpenTelemetryClientFilter.cs | 5 +++-- .../MagicOnionOpenTelemetryOption.cs | 11 ++++++++--- .../MagicOnionOpenTelemetryTracerFilterAttributes.cs | 8 ++++---- .../MagicOnionServerBuilderExtensions.cs | 1 - src/MagicOnion.Server.OpenTelemetry/RpcScope.cs | 5 ++++- 8 files changed, 22 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 127f28cb5..392e5b6d6 100644 --- a/README.md +++ b/README.md @@ -1580,7 +1580,7 @@ I believe that this can be easily and effectively applied to sending a large num ## Experimentals ### OpenTelemetry -MagicOnion.OpenTelemetry is implementation of [open\-telemetry/opentelemetry\-dotnet: OpenTelemetry \.NET SDK](https://github.com/open-telemetry/opentelemetry-dotnet), so you can use any OpenTelemetry exporter, like [Prometheus](https://prometheus.io/), [StackDriver](https://cloud.google.com/stackdriver/pricing), [Zipkin](https://zipkin.io/) and others. +MagicOnion.OpenTelemetry is implementation of [open\-telemetry/opentelemetry\-dotnet: OpenTelemetry \.NET SDK](https://github.com/open-telemetry/opentelemetry-dotnet), so you can use any OpenTelemetry exporter, like [Jaeger](https://www.jaegertracing.io/), [Zipkin](https://zipkin.io/), [StackDriver](https://cloud.google.com/stackdriver) and others. See details at [MagicOnion.Server.OpenTelemetry](src/MagicOnion.Server.OpenTelemetry) diff --git a/src/MagicOnion.Server.OpenTelemetry/Internal/OpenTelemetryHelper.cs b/src/MagicOnion.Server.OpenTelemetry/Internal/OpenTelemetryHelper.cs index c1e00117f..6f7b69f5e 100644 --- a/src/MagicOnion.Server.OpenTelemetry/Internal/OpenTelemetryHelper.cs +++ b/src/MagicOnion.Server.OpenTelemetry/Internal/OpenTelemetryHelper.cs @@ -2,7 +2,7 @@ namespace MagicOnion.Server.OpenTelemetry.Internal { - public static class OpenTelemetryHelper + internal static class OpenTelemetryHelper { /// /// Convert gRPC StatusCode to OpenTelemetry Status. diff --git a/src/MagicOnion.Server.OpenTelemetry/Internal/SemanticConventions.cs b/src/MagicOnion.Server.OpenTelemetry/Internal/SemanticConventions.cs index d84eaf494..0b1eaf195 100644 --- a/src/MagicOnion.Server.OpenTelemetry/Internal/SemanticConventions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/Internal/SemanticConventions.cs @@ -6,6 +6,7 @@ namespace MagicOnion.Server.OpenTelemetry.Internal internal static class SemanticConventions { // tag spec: https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/rpc.md#grpc + public const string AttributeServiceName = "service.name"; public const string AttributeException = "exception"; public const string AttributeHttpHost = "http.host"; diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs index b9b3e3502..8b72dbe1d 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryClientFilter.cs @@ -27,7 +27,7 @@ public MagicOnionOpenTelemetryClientFilter(ActivitySource activitySource, MagicO public async ValueTask SendAsync(RequestContext context, Func> next) { var rpcService = context.MethodPath.Split('/')[0]; - using var rpcScope = new ClientRpcScope(rpcService, context.MethodPath, context, source); + using var rpcScope = new ClientRpcScope(rpcService, context.MethodPath, context, source, options); rpcScope.SetTags(options.TracingTags); try @@ -51,7 +51,8 @@ public async ValueTask SendAsync(RequestContext context, Func - /// Application specific OpenTelemetry Tracing tags + /// ServiceName for Tracer. Especially Zipkin use service.name tag to identify service name. /// - public Dictionary TracingTags { get; set; } = new Dictionary(); + /// input to tag `service.name` + public string ServiceName { get; set; } /// /// Expose RpsScope to the ServiceContext.Items. RpsScope key begin with .TraceContext /// public bool ExposeRpcScope { get; set; } = true; + + /// + /// Application specific OpenTelemetry Tracing tags + /// + public Dictionary TracingTags { get; set; } = new Dictionary(); } } \ No newline at end of file diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs index 9cfd1dd65..1ef65c798 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionOpenTelemetryTracerFilterAttributes.cs @@ -40,7 +40,7 @@ public MagicOnionOpenTelemetryTracerFilterAttribute(ActivitySource activitySourc public override async ValueTask Invoke(ServiceContext context, Func next) { - using var rpcScope = new ServerRpcScope(context.ServiceType.Name, context.CallContext.Method.TrimStart('/'), context.CallContext, source); + using var rpcScope = new ServerRpcScope(context.ServiceType.Name, context.CallContext.Method.TrimStart('/'), context.CallContext, source, options); if (options.ExposeRpcScope) { @@ -114,7 +114,7 @@ public MagicOnionOpenTelemetryStreamingTracerFilterAttribute(ActivitySource acti public override async ValueTask Invoke(StreamingHubContext context, Func next) { - using var rpcScope = new ServerRpcScope(context.ServiceContext.ServiceType.Name, context.Path, context.ServiceContext.CallContext, source); + using var rpcScope = new ServerRpcScope(context.ServiceContext.ServiceType.Name, context.Path, context.ServiceContext.CallContext, source, options); if (options.ExposeRpcScope) { @@ -161,8 +161,8 @@ public override async ValueTask Invoke(StreamingHubContext context, Func @@ -96,6 +98,7 @@ protected void SetActivity(Activity activity) return; } + this.activity.SetTag(SemanticConventions.AttributeServiceName, ServiceName); this.activity.SetTag(SemanticConventions.AttributeRpcSystem, "grpc"); this.activity.SetTag(SemanticConventions.AttributeRpcService, RpcService); this.activity.SetTag(SemanticConventions.AttributeRpcMethod, RpcMethod); From de4427a081bde7f250515b624c88878e5a0ca1c9 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Thu, 3 Jun 2021 04:47:58 +0900 Subject: [PATCH 12/18] feat: null check --- .../MagicOnionServerBuilderExtensions.cs | 16 +++++++--------- src/MagicOnion.Server.OpenTelemetry/RpcScope.cs | 5 +++++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs index 7551f4a41..b5818d043 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs @@ -40,7 +40,12 @@ public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBu if (builder == null) throw new ArgumentNullException(nameof(builder)); var serviceProvider = builder.Services.BuildServiceProvider(); - var activityName = MagicOnionInstrumentation.ActivitySourceName; + // auto listen ActivitySource + var activitySource = new ActivitySource(MagicOnionInstrumentation.ActivitySourceName, MagicOnionInstrumentation.Version.ToString()); + + // DI + builder.Services.AddSingleton(options); + builder.Services.AddSingleton(new MagicOnionActivitySources(activitySource)); // Configure OpenTelemetry Tracer if (configureTracerProvider != null) @@ -48,18 +53,11 @@ public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBu builder.Services.AddOpenTelemetryTracing(configure => { // ActivitySourceName must match to TracerName. - configure.AddSource(activityName); + configure.AddSource(MagicOnionInstrumentation.ActivitySourceName); configureTracerProvider?.Invoke(options, serviceProvider, configure); }); } - // auto listen ActivitySource - var activitySource = new ActivitySource(activityName, MagicOnionInstrumentation.Version.ToString()); - - // DI - builder.Services.AddSingleton(options); - builder.Services.AddSingleton(new MagicOnionActivitySources(activitySource)); - return builder; } diff --git a/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs b/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs index fc78fdb77..51c7b55b5 100644 --- a/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs +++ b/src/MagicOnion.Server.OpenTelemetry/RpcScope.cs @@ -106,6 +106,11 @@ protected void SetActivity(Activity activity) public void SetTags(IDictionary tags) { + if (activity == null) + { + return; + } + foreach (var tag in tags) { activity.SetTag(tag.Key, tag.Value); From 86e976a938ea69b1b54e09897921adb6d49b1a84 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Mon, 7 Jun 2021 12:32:43 +0900 Subject: [PATCH 13/18] feat: split OpenTelemetry and MagicOnion Instrumentation --- .../MagicOnionServerBuilderExtensions.cs | 73 ++++++++----------- .../TracerProviderBuilderExtensions.cs | 20 +++++ 2 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs index b5818d043..7e33510d5 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs @@ -1,62 +1,44 @@ using System; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using OpenTelemetry.Trace; using System.Diagnostics; using MagicOnion.Server.OpenTelemetry.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace MagicOnion.Server.OpenTelemetry { /// MagicOnion extensions for Microsoft.Extensions.Hosting classes public static class MagicOnionServerBuilderExtensions { - /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. - public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, - string configurationName = "") + /// + /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. + /// + /// + /// + /// + public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, string configurationName = "MagicOnion:OpenTelemetry") { - var options = BindMagicOnionOpenTelemetryOptions(builder, configurationName); + var options = CreateDefaultOptions(builder, configurationName); return AddOpenTelemetry(builder, options); } - /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. - public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, - MagicOnionOpenTelemetryOptions options) - { - return AddOpenTelemetry(builder, options, null); - } - /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. - public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, - Action configureTracerFactory, - string configurationName = "") - { - var options = BindMagicOnionOpenTelemetryOptions(builder, configurationName); - return AddOpenTelemetry(builder, options, configureTracerFactory); - } - /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. - public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, - MagicOnionOpenTelemetryOptions options, - Action configureTracerProvider) + + /// + /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. + /// + /// + /// + /// + public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); if (builder == null) throw new ArgumentNullException(nameof(builder)); - var serviceProvider = builder.Services.BuildServiceProvider(); - // auto listen ActivitySource + // listen ActivitySource var activitySource = new ActivitySource(MagicOnionInstrumentation.ActivitySourceName, MagicOnionInstrumentation.Version.ToString()); // DI - builder.Services.AddSingleton(options); - builder.Services.AddSingleton(new MagicOnionActivitySources(activitySource)); - - // Configure OpenTelemetry Tracer - if (configureTracerProvider != null) - { - builder.Services.AddOpenTelemetryTracing(configure => - { - // ActivitySourceName must match to TracerName. - configure.AddSource(MagicOnionInstrumentation.ActivitySourceName); - configureTracerProvider?.Invoke(options, serviceProvider, configure); - }); - } + builder.Services.TryAddSingleton(options); + builder.Services.TryAddSingleton(new MagicOnionActivitySources(activitySource)); return builder; } @@ -67,13 +49,18 @@ public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBu /// /// /// - private static MagicOnionOpenTelemetryOptions BindMagicOnionOpenTelemetryOptions(IMagicOnionServerBuilder builder, string configurationName) + private static MagicOnionOpenTelemetryOptions CreateDefaultOptions(IMagicOnionServerBuilder builder, string configurationName) { - var name = !string.IsNullOrEmpty(configurationName) ? configurationName : "MagicOnion:OpenTelemetry"; var serviceProvider = builder.Services.BuildServiceProvider(); var config = serviceProvider.GetService(); var options = new MagicOnionOpenTelemetryOptions(); - config.GetSection(name).Bind(options); + + var section = config.GetSection(configurationName); + if (section == null) + { + throw new ArgumentOutOfRangeException($"{configurationName} not exists in {nameof(IConfiguration)}."); + } + section.Bind(options); return options; } } diff --git a/src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs new file mode 100644 index 000000000..11d65e2ab --- /dev/null +++ b/src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -0,0 +1,20 @@ +using System; +using OpenTelemetry.Trace; +using MagicOnion.Server.OpenTelemetry.Internal; + +namespace MagicOnion.Server.OpenTelemetry +{ + public static class TracerProviderBuilderExtensions + { + public static TracerProviderBuilder AddMagicOnionInstrumentation( + this TracerProviderBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.AddSource(MagicOnionInstrumentation.ActivitySourceName); + } + } +} From 48ccb27d19e066d5fb7e8938273a38c54138a552 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Mon, 7 Jun 2021 18:23:13 +0900 Subject: [PATCH 14/18] fix: work on linux --- .../MagicOnionServerBuilderExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs index 7e33510d5..30d0957b2 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs @@ -16,10 +16,10 @@ public static class MagicOnionServerBuilderExtensions /// /// /// - public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, string configurationName = "MagicOnion:OpenTelemetry") + public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBuilder builder, string configurationName = "MagicOnion:OpenTelemetry") { var options = CreateDefaultOptions(builder, configurationName); - return AddOpenTelemetry(builder, options); + return UseOpenTelemetry(builder, options); } /// @@ -28,7 +28,7 @@ public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBu /// /// /// - public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options) + public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); if (builder == null) throw new ArgumentNullException(nameof(builder)); From 9e475b5f6dff6d268c613432cad9c9893d378728 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Mon, 7 Jun 2021 18:23:55 +0900 Subject: [PATCH 15/18] feat: remove OpenTelemetry.Extensions.Hosting reference --- .../MagicOnion.Server.OpenTelemetry.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj index b2a352dd2..b30273859 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj @@ -27,7 +27,6 @@ - From 1036f3b6a5884db6a3fe62acb8cb3daa6e9f5592 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Mon, 7 Jun 2021 19:30:05 +0900 Subject: [PATCH 16/18] refactor: Sample App for basic usage --- .../MagicOnionServerBuilderExtensions.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs index 30d0957b2..e26e55065 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs @@ -14,11 +14,11 @@ public static class MagicOnionServerBuilderExtensions /// Configures OpenTelemetry to listen for the Activities created by the MagicOnion Filter. /// /// - /// + /// /// - public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBuilder builder, string configurationName = "MagicOnion:OpenTelemetry") + public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBuilder builder, string overrideServiceName = "") { - var options = CreateDefaultOptions(builder, configurationName); + var options = CreateDefaultOptions(builder, overrideServiceName); return UseOpenTelemetry(builder, options); } @@ -47,20 +47,23 @@ public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBu /// Generate MagicOnionTelemetryOptions and configure if configuration exists. /// /// - /// + /// /// - private static MagicOnionOpenTelemetryOptions CreateDefaultOptions(IMagicOnionServerBuilder builder, string configurationName) + private static MagicOnionOpenTelemetryOptions CreateDefaultOptions(IMagicOnionServerBuilder builder, string overrideServiceName) { + const string configKey = "MagicOnion:OpenTelemetry"; var serviceProvider = builder.Services.BuildServiceProvider(); var config = serviceProvider.GetService(); var options = new MagicOnionOpenTelemetryOptions(); - var section = config.GetSection(configurationName); + var section = config.GetSection(configKey); if (section == null) + throw new ArgumentOutOfRangeException($"{configKey} not exists in {nameof(IConfiguration)}."); + section.Bind(options); + if (!string.IsNullOrEmpty(overrideServiceName)) { - throw new ArgumentOutOfRangeException($"{configurationName} not exists in {nameof(IConfiguration)}."); + options.ServiceName = overrideServiceName; } - section.Bind(options); return options; } } From 08fd41c3a33b4b529463f487a17bb616b45adf07 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Mon, 7 Jun 2021 19:31:08 +0900 Subject: [PATCH 17/18] feat: bump Suffix to 1.1.0-beta2 --- .../MagicOnion.Server.OpenTelemetry.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj index b30273859..b04333077 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnion.Server.OpenTelemetry.csproj @@ -18,8 +18,8 @@ MagicOnion.Server.OpenTelemetry Telemetry Extensions of MagicOnion. $(PackageTags);OpenTelemetry - - rc4-1.0.0 + + beta2-1.1.0 From 04ef25a6577291c676e111299ae79e07565caa08 Mon Sep 17 00:00:00 2001 From: Ikiru Yoshizaki <3856350+guitarrapc@users.noreply.github.com> Date: Mon, 7 Jun 2021 19:34:52 +0900 Subject: [PATCH 18/18] refactor: back to AddOpenTelemetry --- .../MagicOnionServerBuilderExtensions.cs | 6 +++--- .../TracerProviderBuilderExtensions.cs | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs index e26e55065..d53a00c4f 100644 --- a/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/MagicOnionServerBuilderExtensions.cs @@ -16,10 +16,10 @@ public static class MagicOnionServerBuilderExtensions /// /// /// - public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBuilder builder, string overrideServiceName = "") + public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, string overrideServiceName = "") { var options = CreateDefaultOptions(builder, overrideServiceName); - return UseOpenTelemetry(builder, options); + return AddOpenTelemetry(builder, options); } /// @@ -28,7 +28,7 @@ public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBu /// /// /// - public static IMagicOnionServerBuilder UseOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options) + public static IMagicOnionServerBuilder AddOpenTelemetry(this IMagicOnionServerBuilder builder, MagicOnionOpenTelemetryOptions options) { if (options == null) throw new ArgumentNullException(nameof(options)); if (builder == null) throw new ArgumentNullException(nameof(builder)); diff --git a/src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs b/src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs index 11d65e2ab..f7dc291e6 100644 --- a/src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs +++ b/src/MagicOnion.Server.OpenTelemetry/TracerProviderBuilderExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using OpenTelemetry.Trace; using MagicOnion.Server.OpenTelemetry.Internal; @@ -6,8 +6,7 @@ namespace MagicOnion.Server.OpenTelemetry { public static class TracerProviderBuilderExtensions { - public static TracerProviderBuilder AddMagicOnionInstrumentation( - this TracerProviderBuilder builder) + public static TracerProviderBuilder AddMagicOnionInstrumentation(this TracerProviderBuilder builder) { if (builder == null) {