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 e8ce71c
Show file tree
Hide file tree
Showing 12 changed files with 523 additions and 154 deletions.
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(
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 e8ce71c

Please sign in to comment.