From 53891e64671680f3b8dbbd2ba1f8bfec4fa0db10 Mon Sep 17 00:00:00 2001 From: Tiago Araujo Date: Tue, 30 Jul 2024 09:32:23 +0100 Subject: [PATCH] feat: Add StackExchangeRedis Meter --- .../CHANGELOG.md | 9 ++ .../Implementation/RedisMetrics.cs | 57 ++++++++ ...r.cs => RedisProfilerEntryInstrumenter.cs} | 124 ++++++++++++------ .../README.md | 26 +++- ...kExchangeRedisConnectionInstrumentation.cs | 9 +- .../StackExchangeRedisInstrumentation.cs | 21 ++- ...kExchangeRedisInstrumentationExtensions.cs | 120 +++++++++++++++++ ...angeRedisMeterProviderBuilderExtensions.cs | 90 +++++++++++++ ...geRedisTracerProviderBuilderExtensions.cs} | 72 +--------- src/Shared/SemanticConventions.cs | 1 + ...ilerEntryInstrumenterConfigurationTests.cs | 91 +++++++++++++ ...=> RedisProfilerEntryInstrumenterTests.cs} | 56 ++++---- ...kExchangeRedisCallsInstrumentationTests.cs | 10 +- 13 files changed, 532 insertions(+), 154 deletions(-) create mode 100644 src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisMetrics.cs rename src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/{RedisProfilerEntryToActivityConverter.cs => RedisProfilerEntryInstrumenter.cs} (60%) create mode 100644 src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentationExtensions.cs create mode 100644 src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisMeterProviderBuilderExtensions.cs rename src/OpenTelemetry.Instrumentation.StackExchangeRedis/{TracerProviderBuilderExtensions.cs => StackExchangeRedisTracerProviderBuilderExtensions.cs} (70%) create mode 100644 test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryInstrumenterConfigurationTests.cs rename test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/{RedisProfilerEntryToActivityConverterTests.cs => RedisProfilerEntryInstrumenterTests.cs} (64%) diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md index 60254ac153..0ed3f09644 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +## 1.9.0-beta.2 + +Released 2024-Jul-30 + +* Add `OpenTelemetry.Instrumentation.StackExchangeRedis` Meter +([#1982](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/1982)) +* New Metrics: `redis.client.request.duration`, +`redis.client.request.waiting_response`, `redis.client.request.time_in_queue` + ## 1.9.0-beta.1 Released 2024-Jul-23 diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisMetrics.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisMetrics.cs new file mode 100644 index 0000000000..17010ba818 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisMetrics.cs @@ -0,0 +1,57 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Metrics; +using System.Reflection; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation; + +internal class RedisMetrics : IDisposable +{ + internal const string MetricRequestDurationName = "redis.client.request.duration"; + internal const string MetricWaitingResponseName = "redis.client.request.waiting_response"; + internal const string MetricTimeInQueueName = "redis.client.request.time_in_queue"; + + internal static readonly Assembly Assembly = typeof(StackExchangeRedisInstrumentation).Assembly; + internal static readonly AssemblyName AssemblyName = Assembly.GetName(); + internal static readonly string InstrumentationName = AssemblyName.Name; + internal static readonly string InstrumentationVersion = Assembly.GetPackageVersion(); + + private readonly Meter meter; + + public RedisMetrics() + { + this.meter = new Meter(InstrumentationName, InstrumentationVersion); + + this.QueueHistogram = this.meter.CreateHistogram( + MetricTimeInQueueName, + unit: "s", + description: "Total time the redis request was waiting in queue before being sent to the server."); + + this.WaitingResponseHistogram = this.meter.CreateHistogram( + MetricWaitingResponseName, + unit: "s", + description: "Duration of redis requests since sent the request to receive the response."); + + this.RequestHistogram = this.meter.CreateHistogram( + MetricRequestDurationName, + unit: "s", + description: "Total client request duration, including processing, queue and server duration."); + } + + public static RedisMetrics Instance { get; } = new RedisMetrics(); + + public Histogram QueueHistogram { get; } + + public Histogram WaitingResponseHistogram { get; } + + public Histogram RequestHistogram { get; } + + public bool Enabled => RequestHistogram.Enabled; + + public void Dispose() + { + this.meter.Dispose(); + } +} diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryInstrumenter.cs similarity index 60% rename from src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs rename to src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryInstrumenter.cs index 5b3a338383..3ada894092 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryToActivityConverter.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryInstrumenter.cs @@ -14,7 +14,7 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation; -internal static class RedisProfilerEntryToActivityConverter +internal static class RedisProfilerEntryInstrumenter { private static readonly Lazy> MessageDataGetter = new(() => { @@ -73,7 +73,11 @@ static bool GetCommandAndKey( }); }); - public static Activity? ProfilerCommandToActivity(Activity? parentActivity, IProfiledCommand command, StackExchangeRedisInstrumentationOptions options) + public static Activity? ProfilerCommandInstrument( + Activity? parentActivity, + IProfiledCommand command, + RedisMetrics metrics, + StackExchangeRedisInstrumentationOptions options) { var name = command.Command; // Example: SET; if (string.IsNullOrEmpty(name)) @@ -88,30 +92,36 @@ static bool GetCommandAndKey( StackExchangeRedisConnectionInstrumentation.CreationTags, startTime: command.CommandCreated); - if (activity == null) + if (activity is null && metrics.Enabled is false) { return null; } - activity.SetEndTime(command.CommandCreated + command.ElapsedTime); + activity?.SetEndTime(command.CommandCreated + command.ElapsedTime); + var meterTags = metrics.Enabled ? + (IList>)new TagList(StackExchangeRedisConnectionInstrumentation.CreationTags.ToArray()) : + default; - if (activity.IsAllDataRequested) - { - // see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md + // see https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/database.md - // Timing example: - // command.CommandCreated; //2019-01-10 22:18:28Z + // Timing example: + // command.CommandCreated; //2019-01-10 22:18:28Z - // command.CreationToEnqueued; // 00:00:32.4571995 - // command.EnqueuedToSending; // 00:00:00.0352838 - // command.SentToResponse; // 00:00:00.0060586 - // command.ResponseToCompletion; // 00:00:00.0002601 + // command.CreationToEnqueued; // 00:00:32.4571995 + // command.EnqueuedToSending; // 00:00:00.0352838 + // command.SentToResponse; // 00:00:00.0060586 + // command.ResponseToCompletion; // 00:00:00.0002601 - // Total: - // command.ElapsedTime; // 00:00:32.4988020 + // Total: + // command.ElapsedTime; // 00:00:32.4988020 - activity.SetTag(StackExchangeRedisConnectionInstrumentation.RedisFlagsKeyName, command.Flags.ToString()); + var flags = command.Flags.ToString(); + activity?.SetTag(SemanticConventions.AttributeDbRedisFlagsKeyName, flags); + meterTags?.Add(SemanticConventions.AttributeDbRedisFlagsKeyName, flags); + meterTags?.Add(SemanticConventions.AttributeDbStatement, command.Command ?? string.Empty); + if (activity is not null) + { if (options.SetVerboseDatabaseStatements) { var (commandAndKey, script) = MessageDataGetter.Value.Invoke(command); @@ -135,55 +145,93 @@ static bool GetCommandAndKey( // Example: "db.statement": SET; activity.SetTag(SemanticConventions.AttributeDbStatement, command.Command); } + } - if (command.EndPoint != null) + if (command.EndPoint != null) + { + if (command.EndPoint is IPEndPoint ipEndPoint) { - if (command.EndPoint is IPEndPoint ipEndPoint) - { - activity.SetTag(SemanticConventions.AttributeNetPeerIp, ipEndPoint.Address.ToString()); - activity.SetTag(SemanticConventions.AttributeNetPeerPort, ipEndPoint.Port); - } - else if (command.EndPoint is DnsEndPoint dnsEndPoint) - { - activity.SetTag(SemanticConventions.AttributeNetPeerName, dnsEndPoint.Host); - activity.SetTag(SemanticConventions.AttributeNetPeerPort, dnsEndPoint.Port); - } - else - { - activity.SetTag(SemanticConventions.AttributePeerService, command.EndPoint.ToString()); - } + var ip = ipEndPoint.Address.ToString(); + var port = ipEndPoint.Port; + + activity?.SetTag(SemanticConventions.AttributeNetPeerIp, ip); + activity?.SetTag(SemanticConventions.AttributeNetPeerPort, port); + + meterTags?.Add(SemanticConventions.AttributeNetPeerIp, ip); + meterTags?.Add(SemanticConventions.AttributeNetPeerPort, port); + } + else if (command.EndPoint is DnsEndPoint dnsEndPoint) + { + var host = dnsEndPoint.Host; + var port = dnsEndPoint.Port; + + activity?.SetTag(SemanticConventions.AttributeNetPeerName, host); + activity?.SetTag(SemanticConventions.AttributeNetPeerPort, port); + + meterTags?.Add(SemanticConventions.AttributeNetPeerName, host); + meterTags?.Add(SemanticConventions.AttributeNetPeerPort, port); } + else + { + var service = command.EndPoint.ToString(); + + activity?.SetTag(SemanticConventions.AttributePeerService, service); + meterTags?.Add(SemanticConventions.AttributePeerService, service); + } + } - activity.SetTag(StackExchangeRedisConnectionInstrumentation.RedisDatabaseIndexKeyName, command.Db); + var db = command.Db; + activity?.SetTag(SemanticConventions.AttributeDbRedisDatabaseIndex, db); + meterTags?.Add(SemanticConventions.AttributeDbRedisDatabaseIndex, db); - // TODO: deal with the re-transmission - // command.RetransmissionOf; - // command.RetransmissionReason; + // TODO: deal with the re-transmission + // command.RetransmissionOf; + // command.RetransmissionReason; + if (activity?.IsAllDataRequested ?? false) + { var enqueued = command.CommandCreated.Add(command.CreationToEnqueued); var send = enqueued.Add(command.EnqueuedToSending); var response = send.Add(command.SentToResponse); + var completion = send.Add(command.ResponseToCompletion); if (options.EnrichActivityWithTimingEvents) { activity.AddEvent(new ActivityEvent("Enqueued", enqueued)); activity.AddEvent(new ActivityEvent("Sent", send)); activity.AddEvent(new ActivityEvent("ResponseReceived", response)); + activity.AddEvent(new ActivityEvent("Completion", completion)); } options.Enrich?.Invoke(activity, command); } - activity.Stop(); + if (metrics.Enabled && meterTags is TagList meterTagList) + { + metrics.QueueHistogram.Record(command.EnqueuedToSending.TotalSeconds, meterTagList.ToArray()); + metrics.WaitingResponseHistogram.Record(command.SentToResponse.TotalSeconds, meterTagList.ToArray()); + metrics.RequestHistogram.Record(command.ElapsedTime.TotalSeconds, meterTagList.ToArray()); + } + + activity?.Stop(); return activity; } - public static void DrainSession(Activity? parentActivity, IEnumerable sessionCommands, StackExchangeRedisInstrumentationOptions options) + private static void Add(this IList> tags, string ket, object? value) + { + tags?.Add(new KeyValuePair(ket, value)); + } + + public static void DrainSession( + Activity? parentActivity, + IEnumerable sessionCommands, + RedisMetrics redisMetrics, + StackExchangeRedisInstrumentationOptions options) { foreach (var command in sessionCommands) { - ProfilerCommandToActivity(parentActivity, command, options); + ProfilerCommandInstrument(parentActivity, command, redisMetrics, options); } } diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md index 947c818153..4fdcf03e8e 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md @@ -8,7 +8,7 @@ This is an [Instrumentation Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), which instruments [StackExchange.Redis](https://www.nuget.org/packages/StackExchange.Redis/) -and collects traces about outgoing calls to Redis. +and collects traces and metrics about outgoing calls to Redis. > [!NOTE] > This component is based on the OpenTelemetry semantic conventions for @@ -36,10 +36,12 @@ dotnet add package OpenTelemetry.Instrumentation.StackExchangeRedis ## Step 2: Enable StackExchange.Redis Instrumentation at application startup StackExchange.Redis instrumentation must be enabled at application startup. -`AddRedisInstrumentation` method on `TracerProviderBuilder` must be called to -enable Redis instrumentation, passing the `IConnectionMultiplexer` instance used -to make Redis calls. Only those Redis calls made using the same instance of the -`IConnectionMultiplexer` will be instrumented. +`AddRedisInstrumentation` method on `TracerProviderBuilder` and/or +`MeterProviderBuilder` must be called to enable Redis instrumentation, passing +the `IConnectionMultiplexer` instance used to make Redis calls. Only those +Redis calls made using the same instance of the `IConnectionMultiplexer` will +be instrumented. Once tracing and metrics are enabled, any instrumented +connection will export both signals. The following example demonstrates adding StackExchange.Redis instrumentation to a console application. This example also sets up the OpenTelemetry Console @@ -61,6 +63,11 @@ public class Program .AddRedisInstrumentation(connection) .AddConsoleExporter() .Build(); + + using var tracerProvider = Sdk.CreateMeterProviderBuilder() + .AddRedisInstrumentation() + .AddConsoleExporter() + .Build(); } } ``` @@ -88,6 +95,10 @@ using var connection = ConnectionMultiplexer.Connect("localhost:6379"); using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddRedisInstrumentation(connection) .Build(); + +using var tracerProvider = Sdk.CreateMeterProviderBuilder() + .AddRedisInstrumentation() + .Build(); ``` Whatever connection is specified will be collected by OpenTelemetry. @@ -163,6 +174,9 @@ StackExchange.Redis by default does not give detailed database statements like what key or script was used during an operation. The `SetVerboseDatabaseStatements` option can be used to enable gathering this more detailed information. +`SetVerboseDatabaseStatements` is not applied to metrics, only the command is +defined in the statement attribute. + The following example shows how to use `SetVerboseDatabaseStatements`. ```csharp @@ -181,6 +195,8 @@ raw `IProfiledCommand` object. The `Enrich` action is called only when `activity.IsAllDataRequested` is `true`. It contains the activity itself (which can be enriched), and the source profiled command object. +The `Enrich` action is not applied for metrics. + The following code snippet shows how to add additional tags using `Enrich`. ```csharp diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs index 08711711b7..4029c59b48 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisConnectionInstrumentation.cs @@ -17,8 +17,6 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis; /// internal sealed class StackExchangeRedisConnectionInstrumentation : IDisposable { - internal const string RedisDatabaseIndexKeyName = "db.redis.database_index"; - internal const string RedisFlagsKeyName = "db.redis.flags"; internal static readonly Assembly Assembly = typeof(StackExchangeRedisConnectionInstrumentation).Assembly; internal static readonly string ActivitySourceName = Assembly.GetName().Name!; internal static readonly string ActivityName = ActivitySourceName + ".Execute"; @@ -50,6 +48,7 @@ public StackExchangeRedisConnectionInstrumentation( { Guard.ThrowIfNull(connection); + this.Connection = connection; this.options = options ?? new StackExchangeRedisInstrumentationOptions(); this.drainThread = new Thread(this.DrainEntries) @@ -62,6 +61,8 @@ public StackExchangeRedisConnectionInstrumentation( connection.RegisterProfiler(this.GetProfilerSessionsFactory()); } + internal IConnectionMultiplexer Connection { get; } + /// /// Returns session for the Redis calls recording. /// @@ -108,7 +109,7 @@ public void Dispose() internal void Flush() { - RedisProfilerEntryToActivityConverter.DrainSession(null, this.defaultSession.FinishProfiling(), this.options); + RedisProfilerEntryInstrumenter.DrainSession(null, this.defaultSession.FinishProfiling(), RedisMetrics.Instance, this.options); foreach (var entry in this.Cache) { @@ -120,7 +121,7 @@ internal void Flush() } ProfilingSession session = entry.Value.Session; - RedisProfilerEntryToActivityConverter.DrainSession(parent, session.FinishProfiling(), this.options); + RedisProfilerEntryInstrumenter.DrainSession(parent, session.FinishProfiling(), RedisMetrics.Instance, this.options); this.Cache.TryRemove((entry.Key.TraceId, entry.Key.SpanId), out _); } } diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentation.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentation.cs index 4436a42df4..e1cf64246e 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentation.cs @@ -20,7 +20,7 @@ internal StackExchangeRedisInstrumentation( this.options = options; } - internal List InstrumentedConnections { get; } = new(); + internal List InstrumentedConnections { get; } = []; /// /// Adds an to the instrumentation. @@ -45,9 +45,13 @@ public IDisposable AddConnection(string name, IConnectionMultiplexer connection) lock (this.InstrumentedConnections) { - var instrumentation = new StackExchangeRedisConnectionInstrumentation(connection, name, options); + var instrumentation = this.InstrumentedConnections.FirstOrDefault(i => i.Connection == connection); + if (instrumentation is null) + { + instrumentation = new StackExchangeRedisConnectionInstrumentation(connection, name, options); - this.InstrumentedConnections.Add(instrumentation); + this.InstrumentedConnections.Add(instrumentation); + } return new StackExchangeRedisConnectionInstrumentationRegistration(() => { @@ -76,15 +80,10 @@ public void Dispose() } } - private sealed class StackExchangeRedisConnectionInstrumentationRegistration : IDisposable + private sealed class StackExchangeRedisConnectionInstrumentationRegistration( + Action disposalAction) : IDisposable { - private readonly Action disposalAction; - - public StackExchangeRedisConnectionInstrumentationRegistration( - Action disposalAction) - { - this.disposalAction = disposalAction; - } + private readonly Action disposalAction = disposalAction; public void Dispose() { diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentationExtensions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentationExtensions.cs new file mode 100644 index 0000000000..6f2d7d8480 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisInstrumentationExtensions.cs @@ -0,0 +1,120 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using StackExchange.Redis; + +namespace OpenTelemetry.Instrumentation.StackExchangeRedis; + +public static class StackExchangeRedisInstrumentationExtensions +{ + /// + /// Registers a callback for configuring Redis instrumentation. + /// + /// being configured. + /// Callback to configure instrumentation. + /// The instance of to chain the calls. + public static TracerProviderBuilder ConfigureRedisInstrumentation( + this TracerProviderBuilder builder, + Action configure) + { + Guard.ThrowIfNull(configure); + + return ConfigureRedisInstrumentation(builder, (sp, instrumentation) => configure(instrumentation)); + } + + /// + /// Registers a callback for configuring Redis instrumentation. + /// + /// being configured. + /// Callback to configure instrumentation. + /// The instance of to chain the calls. + public static TracerProviderBuilder ConfigureRedisInstrumentation( + this TracerProviderBuilder builder, + Action configure) + { + Guard.ThrowIfNull(configure); + + if (builder is not IDeferredTracerProviderBuilder deferredTracerProviderBuilder) + { + throw new NotSupportedException("ConfigureRedisInstrumentation is not supported on the supplied builder type."); + } + + builder.AddRedisInstrumentationSharedServices(); + + deferredTracerProviderBuilder.Configure( + (sp, builder) => configure(sp, sp.GetRequiredService())); + + return builder; + } + + internal static TracerProviderBuilder AddRedisInstrumentationSharedServices( + this TracerProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + + return builder.ConfigureServices(AddRedisInstrumentationSharedServices); + } + + internal static MeterProviderBuilder AddRedisInstrumentationSharedServices( + this MeterProviderBuilder builder) + { + Guard.ThrowIfNull(builder); + + return builder.ConfigureServices(AddRedisInstrumentationSharedServices); + } + + internal static void AddRedisInstrumentationSharedServices(IServiceCollection services) + { + services.TryAddSingleton( + sp => new StackExchangeRedisInstrumentation( + sp.GetRequiredService>())); + } + + internal static TracerProviderBuilder AddInstrumentation( + this TracerProviderBuilder builder, + string? name, + IConnectionMultiplexer connection, + object? serviceKey) + { + return builder + .AddInstrumentation(InstrumentationFactory(name, connection, serviceKey)); + } + + internal static MeterProviderBuilder AddInstrumentation( + this MeterProviderBuilder builder, + string? name, + IConnectionMultiplexer connection, + object? serviceKey) + { + return builder + .AddInstrumentation(InstrumentationFactory(name, connection, serviceKey)); + } + + internal static Func InstrumentationFactory( + string? name, + IConnectionMultiplexer connection, + object? serviceKey) + { + return sp => + { + var instrumentation = sp.GetRequiredService(); + + connection ??= serviceKey == null + ? sp.GetService() + : sp.GetKeyedService(serviceKey); + + if (connection != null) + { + instrumentation.AddConnection(name, connection); + } + + return instrumentation; + }; + } +} diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisMeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisMeterProviderBuilderExtensions.cs new file mode 100644 index 0000000000..1e13dad7e5 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisMeterProviderBuilderExtensions.cs @@ -0,0 +1,90 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using Microsoft.Extensions.Options; +using OpenTelemetry.Instrumentation.StackExchangeRedis; +using OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation; +using OpenTelemetry.Internal; +using StackExchange.Redis; + +namespace OpenTelemetry.Metrics; + +/// +/// Extension methods to simplify registering of StackExchangeRedis request instrumentation. +/// +public static class StackExchangeRedisMeterProviderBuilderExtensions +{ + /// + /// Enables automatic data collection of outgoing requests to Redis. + /// + /// + /// Note: A will be resolved using the + /// application . + /// + /// being configured. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddRedisInstrumentation( + this MeterProviderBuilder builder) + => AddRedisInstrumentation(builder, name: null, connection: null, serviceKey: null); + + /// + /// Enables automatic data collection of outgoing requests to Redis. + /// + /// being configured. + /// to instrument. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddRedisInstrumentation( + this MeterProviderBuilder builder, + IConnectionMultiplexer connection) + { + Guard.ThrowIfNull(connection); + + return AddRedisInstrumentation(builder, name: null, connection, serviceKey: null); + } + + /// + /// Enables automatic data collection of outgoing requests to Redis. + /// + /// being configured. + /// Optional service key used to retrieve the to instrument from the . + /// The instance of to chain the calls. + public static MeterProviderBuilder AddRedisInstrumentation( + this MeterProviderBuilder builder, + object serviceKey) + { + Guard.ThrowIfNull(serviceKey); + + return AddRedisInstrumentation(builder, name: null, connection: null, serviceKey); + } + + /// + /// Enables automatic data collection of outgoing requests to Redis. + /// + /// + /// Note: If an is not supplied + /// using the parameter it will be + /// resolved using the application . + /// + /// being configured. + /// Optional name which is used when retrieving options. + /// Optional to instrument. + /// Optional service key used to retrieve the to instrument from the . + /// Optional callback to configure options. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddRedisInstrumentation( + this MeterProviderBuilder builder, + string? name, + IConnectionMultiplexer? connection, + object? serviceKey) + { + Guard.ThrowIfNull(builder); + + name ??= Options.DefaultName; + + builder.AddRedisInstrumentationSharedServices(); + + return builder + .AddMeter(RedisMetrics.InstrumentationName) + .AddInstrumentation(name, connection, serviceKey); + } +} diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisTracerProviderBuilderExtensions.cs similarity index 70% rename from src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs rename to src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisTracerProviderBuilderExtensions.cs index d9a450a2a6..12ee54b539 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisTracerProviderBuilderExtensions.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OpenTelemetry.Instrumentation.StackExchangeRedis; using OpenTelemetry.Internal; @@ -13,7 +12,7 @@ namespace OpenTelemetry.Trace; /// /// Extension methods to simplify registering of dependency instrumentation. /// -public static class TracerProviderBuilderExtensions +public static class StackExchangeRedisTracerProviderBuilderExtensions { /// /// Enables automatic data collection of outgoing requests to Redis. @@ -152,73 +151,6 @@ public static TracerProviderBuilder AddRedisInstrumentation( return builder .AddSource(StackExchangeRedisConnectionInstrumentation.ActivitySourceName) - .AddInstrumentation(sp => - { - var instrumentation = sp.GetRequiredService(); - - connection ??= serviceKey == null - ? sp.GetService() - : sp.GetKeyedService(serviceKey); - - if (connection != null) - { - instrumentation.AddConnection(name, connection); - } - - return instrumentation; - }); - } - - /// - /// Registers a callback for configuring Redis instrumentation. - /// - /// being configured. - /// Callback to configure instrumentation. - /// The instance of to chain the calls. - public static TracerProviderBuilder ConfigureRedisInstrumentation( - this TracerProviderBuilder builder, - Action configure) - { - Guard.ThrowIfNull(configure); - - return ConfigureRedisInstrumentation(builder, (sp, instrumentation) => configure(instrumentation)); - } - - /// - /// Registers a callback for configuring Redis instrumentation. - /// - /// being configured. - /// Callback to configure instrumentation. - /// The instance of to chain the calls. - public static TracerProviderBuilder ConfigureRedisInstrumentation( - this TracerProviderBuilder builder, - Action configure) - { - Guard.ThrowIfNull(configure); - - if (builder is not IDeferredTracerProviderBuilder deferredTracerProviderBuilder) - { - throw new NotSupportedException("ConfigureRedisInstrumentation is not supported on the supplied builder type."); - } - - builder.AddRedisInstrumentationSharedServices(); - - deferredTracerProviderBuilder.Configure( - (sp, builder) => configure(sp, sp.GetRequiredService())); - - return builder; - } - - private static TracerProviderBuilder AddRedisInstrumentationSharedServices( - this TracerProviderBuilder builder) - { - Guard.ThrowIfNull(builder); - - return builder.ConfigureServices(services => - { - services.TryAddSingleton( - sp => new StackExchangeRedisInstrumentation( - sp.GetRequiredService>())); - }); + .AddInstrumentation(name, connection, serviceKey); } } diff --git a/src/Shared/SemanticConventions.cs b/src/Shared/SemanticConventions.cs index 9f1c1ee234..10fbbc161c 100644 --- a/src/Shared/SemanticConventions.cs +++ b/src/Shared/SemanticConventions.cs @@ -56,6 +56,7 @@ internal static class SemanticConventions public const string AttributeDbCassandraKeyspace = "db.cassandra.keyspace"; public const string AttributeDbHBaseNamespace = "db.hbase.namespace"; public const string AttributeDbRedisDatabaseIndex = "db.redis.database_index"; + public const string AttributeDbRedisFlagsKeyName = "db.redis.flags"; public const string AttributeDbMongoDbCollection = "db.mongodb.collection"; public const string AttributeRpcSystem = "rpc.system"; diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryInstrumenterConfigurationTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryInstrumenterConfigurationTests.cs new file mode 100644 index 0000000000..bf89561349 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryInstrumenterConfigurationTests.cs @@ -0,0 +1,91 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics; +using OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using StackExchange.Redis; +using Xunit; + +namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.Implementation; + +[Collection("Redis")] +public class RedisProfilerEntryInstrumenterConfigurationTests +{ + private const int MaxTimeToAllowForFlush = 20000; + + private readonly ConnectionMultiplexer connection; + private readonly List exportedItems = []; + + public RedisProfilerEntryInstrumenterConfigurationTests() + { + var connectionOptions = new ConfigurationOptions + { + AbortOnConnectFail = false, + }; + connectionOptions.EndPoints.Add("localhost:6379"); + + this.connection = ConnectionMultiplexer.Connect(connectionOptions); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task RedisProfilerEntryInstrumenter_WhenTracesAndMeters(bool enableTracing, bool enableMeter) + { + var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); + var profiledCommand = new TestProfiledCommand(DateTime.UtcNow); + + var traceProvider = CreateTraceProvider(enableTracing); + var meterProvider = CreateMeterProvider(enableMeter); + + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); + + if (enableTracing) + { + Assert.NotNull(result); + Assert.Equal("SET", result.DisplayName); + } + else + { + Assert.Null(result); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + if (enableMeter) + { + Assert.True(exportedItems.Count >= 3); + } + else + { + Assert.True(exportedItems.Count == 0); + } + } + + private TracerProvider CreateTraceProvider(bool addInstrumentation) + { + var builder = Sdk.CreateTracerProviderBuilder(); + if (addInstrumentation) + { + builder.AddRedisInstrumentation(this.connection); + } + + return builder.Build()!; + } + + private MeterProvider CreateMeterProvider(bool addInstrumentation) + { + var builder = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(exportedItems); + if (addInstrumentation) + { + builder.AddRedisInstrumentation(this.connection); + } + + return builder.Build()!; + } +} diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryInstrumenterTests.cs similarity index 64% rename from test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs rename to test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryInstrumenterTests.cs index bf67721ea8..d653fd34af 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryInstrumenterTests.cs @@ -3,11 +3,12 @@ using System.Diagnostics; using System.Net; -using OpenTelemetry.Instrumentation.StackExchangeRedis.Tests; - #if !NETFRAMEWORK using System.Net.Sockets; #endif +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Instrumentation.StackExchangeRedis.Tests; +using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using StackExchange.Redis; using Xunit; @@ -15,12 +16,12 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation; [Collection("Redis")] -public class RedisProfilerEntryToActivityConverterTests : IDisposable +public class RedisProfilerEntryInstrumenterTests : IDisposable { private readonly ConnectionMultiplexer connection; private readonly TracerProvider tracerProvider; - public RedisProfilerEntryToActivityConverterTests() + public RedisProfilerEntryInstrumenterTests() { var connectionOptions = new ConfigurationOptions { @@ -43,37 +44,40 @@ public void Dispose() } [Fact] - public void ProfilerCommandToActivity_UsesCommandAsName() + public async Task ProfilerCommandInstrument_UsesCommandAsName() { var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); var profiledCommand = new TestProfiledCommand(DateTime.UtcNow); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); Assert.Equal("SET", result.DisplayName); } [Fact] - public void ProfilerCommandToActivity_UsesTimestampAsStartTime() + public void ProfilerCommandInstrument_UsesTimestampAsStartTime() { var now = DateTimeOffset.Now; var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); var profiledCommand = new TestProfiledCommand(now.DateTime); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); Assert.Equal(now, result.StartTimeUtc); } [Fact] - public void ProfilerCommandToActivity_SetsDbTypeAttributeAsRedis() + public void ProfilerCommandInstrument_SetsDbTypeAttributeAsRedis() { var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); var profiledCommand = new TestProfiledCommand(DateTime.UtcNow); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeDbSystem)); @@ -81,12 +85,13 @@ public void ProfilerCommandToActivity_SetsDbTypeAttributeAsRedis() } [Fact] - public void ProfilerCommandToActivity_UsesCommandAsDbStatementAttribute() + public void ProfilerCommandInstrument_UsesCommandAsDbStatementAttribute() { var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); var profiledCommand = new TestProfiledCommand(DateTime.UtcNow); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeDbStatement)); @@ -94,34 +99,37 @@ public void ProfilerCommandToActivity_UsesCommandAsDbStatementAttribute() } [Fact] - public void ProfilerCommandToActivity_UsesFlagsForFlagsAttribute() + public void ProfilerCommandInstrument_UsesFlagsForFlagsAttribute() { var activity = new Activity("redis-profiler"); + + var metrics = new RedisMetrics(); var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, CommandFlags.FireAndForget | CommandFlags.NoRedirect); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); - Assert.NotNull(result.GetTagValue(StackExchangeRedisConnectionInstrumentation.RedisFlagsKeyName)); + Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeDbRedisFlagsKeyName)); #if NET8_0 - Assert.Equal("FireAndForget, NoRedirect", result.GetTagValue(StackExchangeRedisConnectionInstrumentation.RedisFlagsKeyName)); + Assert.Equal("FireAndForget, NoRedirect", result.GetTagValue(SemanticConventions.AttributeDbRedisFlagsKeyName)); #else - Assert.Equal("PreferMaster, FireAndForget, NoRedirect", result.GetTagValue(StackExchangeRedisConnectionInstrumentation.RedisFlagsKeyName)); + Assert.Equal("PreferMaster, FireAndForget, NoRedirect", result.GetTagValue(SemanticConventions.AttributeDbRedisFlagsKeyName)); #endif } [Fact] - public void ProfilerCommandToActivity_UsesIpEndPointAsEndPoint() + public void ProfilerCommandInstrument_UsesIpEndPointAsEndPoint() { long address = 1; int port = 2; var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); IPEndPoint ipLocalEndPoint = new IPEndPoint(address, port); var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, ipLocalEndPoint); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeNetPeerIp)); @@ -131,14 +139,15 @@ public void ProfilerCommandToActivity_UsesIpEndPointAsEndPoint() } [Fact] - public void ProfilerCommandToActivity_UsesDnsEndPointAsEndPoint() + public void ProfilerCommandInstrument_UsesDnsEndPointAsEndPoint() { var dnsEndPoint = new DnsEndPoint("https://opentelemetry.io/", 443); var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, dnsEndPoint); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeNetPeerName)); @@ -149,13 +158,14 @@ public void ProfilerCommandToActivity_UsesDnsEndPointAsEndPoint() #if !NETFRAMEWORK [Fact] - public void ProfilerCommandToActivity_UsesOtherEndPointAsEndPoint() + public void ProfilerCommandInstrument_UsesOtherEndPointAsEndPoint() { var unixEndPoint = new UnixDomainSocketEndPoint("https://opentelemetry.io/"); var activity = new Activity("redis-profiler"); + var metrics = new RedisMetrics(); var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, unixEndPoint); - var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions()); + var result = RedisProfilerEntryInstrumenter.ProfilerCommandInstrument(activity, profiledCommand, metrics, new StackExchangeRedisInstrumentationOptions()); Assert.NotNull(result); Assert.NotNull(result.GetTagValue(SemanticConventions.AttributePeerService)); diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs index 4ede2553fa..e675109f88 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/StackExchangeRedisCallsInstrumentationTests.cs @@ -370,11 +370,15 @@ public void StackExchangeRedis_StackExchangeRedisInstrumentation_Test() { Assert.NotNull(instrumentation); - var registration = instrumentation.AddConnection(connection); + var registration1 = instrumentation.AddConnection(connection); Assert.NotEmpty(instrumentation.InstrumentedConnections); - registration.Dispose(); + var registration2 = instrumentation.AddConnection(connection); + + Assert.Single(instrumentation.InstrumentedConnections); + + registration1.Dispose(); Assert.Empty(instrumentation.InstrumentedConnections); @@ -413,7 +417,7 @@ private static void VerifyActivityData(Activity activity, bool isSet, EndPoint e Assert.Equal(Status.Unset, activity.GetStatus()); Assert.Equal("redis", activity.GetTagValue(SemanticConventions.AttributeDbSystem)); - Assert.Equal(0, activity.GetTagValue(StackExchangeRedisConnectionInstrumentation.RedisDatabaseIndexKeyName)); + Assert.Equal(0, activity.GetTagValue(SemanticConventions.AttributeDbRedisDatabaseIndex)); if (endPoint is IPEndPoint ipEndPoint) {