Skip to content

Commit

Permalink
feat: Add StackExchangeRedis Meter
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagodaraujo committed Jul 30, 2024
1 parent 2da3c6e commit 53891e6
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 154 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<double>(
MetricTimeInQueueName,
unit: "s",
description: "Total time the redis request was waiting in queue before being sent to the server.");

this.WaitingResponseHistogram = this.meter.CreateHistogram<double>(
MetricWaitingResponseName,
unit: "s",
description: "Duration of redis requests since sent the request to receive the response.");

this.RequestHistogram = this.meter.CreateHistogram<double>(
MetricRequestDurationName,
unit: "s",
description: "Total client request duration, including processing, queue and server duration.");
}

public static RedisMetrics Instance { get; } = new RedisMetrics();

public Histogram<double> QueueHistogram { get; }

public Histogram<double> WaitingResponseHistogram { get; }

public Histogram<double> RequestHistogram { get; }

public bool Enabled => RequestHistogram.Enabled;

public void Dispose()
{
this.meter.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation;

internal static class RedisProfilerEntryToActivityConverter
internal static class RedisProfilerEntryInstrumenter
{
private static readonly Lazy<Func<object, (string?, string?)>> MessageDataGetter = new(() =>
{
Expand Down Expand Up @@ -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))
Expand All @@ -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<KeyValuePair<string, object?>>)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);
Expand All @@ -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<IProfiledCommand> sessionCommands, StackExchangeRedisInstrumentationOptions options)
private static void Add(this IList<KeyValuePair<string, object?>> tags, string ket, object? value)
{
tags?.Add(new KeyValuePair<string, object?>(ket, value));
}

public static void DrainSession(

Check failure on line 226 in src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryInstrumenter.cs

View workflow job for this annotation

GitHub Actions / build-test-instrumentation-stackexchangeredis / build-test (ubuntu-latest, net8.0)

Check failure on line 226 in src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryInstrumenter.cs

View workflow job for this annotation

GitHub Actions / build-test-instrumentation-stackexchangeredis / build-test (ubuntu-latest, net6.0)

Check failure on line 226 in src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryInstrumenter.cs

View workflow job for this annotation

GitHub Actions / build-test-instrumentation-stackexchangeredis / build-test (ubuntu-latest, net7.0)

Check failure on line 226 in src/OpenTelemetry.Instrumentation.StackExchangeRedis/Implementation/RedisProfilerEntryInstrumenter.cs

View workflow job for this annotation

GitHub Actions / build-test-instrumentation-stackexchangeredis / build-test (windows-latest, net7.0)

Activity? parentActivity,
IEnumerable<IProfiledCommand> sessionCommands,
RedisMetrics redisMetrics,
StackExchangeRedisInstrumentationOptions options)
{
foreach (var command in sessionCommands)
{
ProfilerCommandToActivity(parentActivity, command, options);
ProfilerCommandInstrument(parentActivity, command, redisMetrics, options);
}
}

Expand Down
26 changes: 21 additions & 5 deletions src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -61,6 +63,11 @@ public class Program
.AddRedisInstrumentation(connection)
.AddConsoleExporter()
.Build();

using var tracerProvider = Sdk.CreateMeterProviderBuilder()
.AddRedisInstrumentation()
.AddConsoleExporter()
.Build();
}
}
```
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis;
/// </summary>
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";
Expand Down Expand Up @@ -50,6 +48,7 @@ public StackExchangeRedisConnectionInstrumentation(
{
Guard.ThrowIfNull(connection);

this.Connection = connection;
this.options = options ?? new StackExchangeRedisInstrumentationOptions();

this.drainThread = new Thread(this.DrainEntries)
Expand All @@ -62,6 +61,8 @@ public StackExchangeRedisConnectionInstrumentation(
connection.RegisterProfiler(this.GetProfilerSessionsFactory());
}

internal IConnectionMultiplexer Connection { get; }

/// <summary>
/// Returns session for the Redis calls recording.
/// </summary>
Expand Down Expand Up @@ -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)
{
Expand All @@ -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 _);
}
}
Expand Down
Loading

0 comments on commit 53891e6

Please sign in to comment.