Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Instrumentation.StackExchangeRedis] Use different source names for connections #2001

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,16 @@ static bool GetCommandAndKey(
});
});

public static Activity? ProfilerCommandToActivity(Activity? parentActivity, IProfiledCommand command, StackExchangeRedisInstrumentationOptions options)
public static Activity? ProfilerCommandToActivity(Activity? parentActivity, IProfiledCommand command, StackExchangeRedisInstrumentationOptions options, string name)
{
var name = command.Command; // Example: SET;
if (string.IsNullOrEmpty(name))
var commandName = command.Command; // Example: SET;
if (string.IsNullOrEmpty(commandName))
{
name = StackExchangeRedisConnectionInstrumentation.ActivityName;
commandName = StackExchangeRedisConnectionInstrumentation.ActivityName;
}

var activity = StackExchangeRedisConnectionInstrumentation.ActivitySource.StartActivity(
name,
var activity = StackExchangeRedisConnectionInstrumentation.GetActivitySource(name).StartActivity(
commandName,
ActivityKind.Client,
parentActivity?.Context ?? default,
StackExchangeRedisConnectionInstrumentation.CreationTags,
Expand Down Expand Up @@ -179,11 +179,11 @@ static bool GetCommandAndKey(
return activity;
}

public static void DrainSession(Activity? parentActivity, IEnumerable<IProfiledCommand> sessionCommands, StackExchangeRedisInstrumentationOptions options)
public static void DrainSession(Activity? parentActivity, IEnumerable<IProfiledCommand> sessionCommands, StackExchangeRedisInstrumentationOptions options, string name)
{
foreach (var command in sessionCommands)
{
ProfilerCommandToActivity(parentActivity, command, options);
ProfilerCommandToActivity(parentActivity, command, options, name);
}
}

Expand Down
32 changes: 32 additions & 0 deletions src/OpenTelemetry.Instrumentation.StackExchangeRedis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,38 @@ the `ConfigureServices` of your `Startup` class. Refer to documentation for
For an ASP.NET application, adding instrumentation is typically done in the
`Global.asax.cs`. Refer to documentation for [OpenTelemetry.Instrumentation.AspNet](../OpenTelemetry.Instrumentation.AspNet/README.md).

### Instrumenting multiple Redis connections

If your application utilizes multiple Redis connections, they can be instrumented separately if desired.
To do this, simply provide a unique `name` to the `AddRedisInstrumentation` method:

```csharp
using OpenTelemetry.Trace;

public class Program
{
public static void Main(string[] args)
{
// Connect to the first server.
var firstConnectionName = "myRedisConnection1";
using var connection1 = ConnectionMultiplexer.Connect("server1:6379");

// Connect to the second server.
var secondConnectionName = "myRedisConnection2";
using var connection2 = ConnectionMultiplexer.Connect("server2:6379");

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddRedisInstrumentation(firstConnectionName, connection1, null)
.AddRedisInstrumentation(secondConnectionName, connection2, null)
.AddConsoleExporter()
.Build();
}
}
```

This will give you the ability to provide different instrumentation options for each Redis connection.
It should also prevent you from receiving duplicate spans in your traces since each Redis connection is now instrumented separately.

## Specify the Redis connection

The following sections cover different ways to specify the `StackExchange.Redis`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal sealed class StackExchangeRedisConnectionInstrumentation : IDisposable
internal static readonly string ActivitySourceName = Assembly.GetName().Name!;
internal static readonly string ActivityName = ActivitySourceName + ".Execute";
internal static readonly ActivitySource ActivitySource = new(ActivitySourceName, Assembly.GetPackageVersion());
internal static readonly ConcurrentDictionary<string, ActivitySource> ActivitySources = new();
internal static readonly IEnumerable<KeyValuePair<string, object?>> CreationTags = new[]
{
new KeyValuePair<string, object?>(SemanticConventions.AttributeDbSystem, "redis"),
Expand All @@ -36,6 +37,7 @@ internal sealed class StackExchangeRedisConnectionInstrumentation : IDisposable
private readonly Thread drainThread;

private readonly ProfilingSession defaultSession = new();
private readonly string name;

/// <summary>
/// Initializes a new instance of the <see cref="StackExchangeRedisConnectionInstrumentation"/> class.
Expand All @@ -51,6 +53,19 @@ public StackExchangeRedisConnectionInstrumentation(
Guard.ThrowIfNull(connection);

this.options = options ?? new StackExchangeRedisInstrumentationOptions();
this.name = name ?? string.Empty;

if (!string.IsNullOrWhiteSpace(name))
{
lock (ActivitySources)
{
if (!ActivitySources.TryGetValue(name!, out var activitySource))
{
activitySource = new($"{GetActivitySourceName(name!)}", Assembly.GetPackageVersion());
ActivitySources.TryAdd(name!, activitySource);
}
}
}

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

public static ActivitySource GetActivitySource(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return ActivitySource;
}

ActivitySource? source;

if (!ActivitySources.TryGetValue(name, out source))
{
source = ActivitySource;
}

return source;
}

public static string GetActivitySourceName(string name)
{
if (!string.IsNullOrWhiteSpace(name))
{
return $"{ActivitySourceName}.{name}";
}

return ActivitySourceName;
}

/// <summary>
/// Returns session for the Redis calls recording.
/// </summary>
Expand Down Expand Up @@ -108,7 +150,7 @@ public void Dispose()

internal void Flush()
{
RedisProfilerEntryToActivityConverter.DrainSession(null, this.defaultSession.FinishProfiling(), this.options);
RedisProfilerEntryToActivityConverter.DrainSession(null, this.defaultSession.FinishProfiling(), this.options, this.name);

foreach (var entry in this.Cache)
{
Expand All @@ -120,7 +162,7 @@ internal void Flush()
}

ProfilingSession session = entry.Value.Session;
RedisProfilerEntryToActivityConverter.DrainSession(parent, session.FinishProfiling(), this.options);
RedisProfilerEntryToActivityConverter.DrainSession(parent, session.FinishProfiling(), this.options, this.name);
this.Cache.TryRemove((entry.Key.TraceId, entry.Key.SpanId), out _);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public static TracerProviderBuilder AddRedisInstrumentation(
/// resolved using the application <see cref="IServiceProvider"/>.
/// </remarks>
/// <param name="builder"><see cref="TracerProviderBuilder"/> being configured.</param>
/// <param name="name">Optional name which is used when retrieving options.</param>
/// <param name="name">Optional name which is used when retrieving options as well as adding the ActivitySource name.</param>
/// <param name="connection">Optional <see cref="IConnectionMultiplexer"/> to instrument.</param>
/// <param name="serviceKey">Optional service key used to retrieve the <see cref="IConnectionMultiplexer"/> to instrument from the <see cref="IServiceProvider" />.</param>
/// <param name="configure">Optional callback to configure options.</param>
Expand All @@ -151,7 +151,7 @@ public static TracerProviderBuilder AddRedisInstrumentation(
}

return builder
.AddSource(StackExchangeRedisConnectionInstrumentation.ActivitySourceName)
.AddSource(StackExchangeRedisConnectionInstrumentation.GetActivitySourceName(name))
.AddInstrumentation(sp =>
{
var instrumentation = sp.GetRequiredService<StackExchangeRedisInstrumentation>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation;
public class RedisProfilerEntryToActivityConverterTests : IDisposable
{
private readonly ConnectionMultiplexer connection;
private readonly TracerProvider tracerProvider;
private TracerProvider tracerProvider; // allowing this to be modified by tests as needed

public RedisProfilerEntryToActivityConverterTests()
{
Expand Down Expand Up @@ -48,20 +48,38 @@ public void ProfilerCommandToActivity_UsesCommandAsName()
var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.Equal("SET", result.DisplayName);
}

[Fact]
public void ProfilerCommandToActivity_UsesActivitySourceForName()
{
// set up tracerProvider to add redis instrumentation with 'name' for ActivitySource
var name = "foo";
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddRedisInstrumentation(name, this.connection, null, null)
.Build()!;

var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), name);

Assert.NotNull(result);
Assert.Equal(StackExchangeRedisConnectionInstrumentation.GetActivitySourceName(name), result.Source.Name);
}

[Fact]
public void ProfilerCommandToActivity_UsesTimestampAsStartTime()
{
var now = DateTimeOffset.Now;
var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(now.DateTime);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.Equal(now, result.StartTimeUtc);
Expand All @@ -73,7 +91,7 @@ public void ProfilerCommandToActivity_SetsDbTypeAttributeAsRedis()
var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeDbSystem));
Expand All @@ -86,7 +104,7 @@ public void ProfilerCommandToActivity_UsesCommandAsDbStatementAttribute()
var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeDbStatement));
Expand All @@ -99,7 +117,7 @@ public void ProfilerCommandToActivity_UsesFlagsForFlagsAttribute()
var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, CommandFlags.FireAndForget | CommandFlags.NoRedirect);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.NotNull(result.GetTagValue(StackExchangeRedisConnectionInstrumentation.RedisFlagsKeyName));
Expand All @@ -121,7 +139,7 @@ public void ProfilerCommandToActivity_UsesIpEndPointAsEndPoint()
IPEndPoint ipLocalEndPoint = new IPEndPoint(address, port);
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, ipLocalEndPoint);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeNetPeerIp));
Expand All @@ -138,7 +156,7 @@ public void ProfilerCommandToActivity_UsesDnsEndPointAsEndPoint()
var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, dnsEndPoint);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.NotNull(result.GetTagValue(SemanticConventions.AttributeNetPeerName));
Expand All @@ -155,7 +173,7 @@ public void ProfilerCommandToActivity_UsesOtherEndPointAsEndPoint()
var activity = new Activity("redis-profiler");
var profiledCommand = new TestProfiledCommand(DateTime.UtcNow, unixEndPoint);

var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions());
var result = RedisProfilerEntryToActivityConverter.ProfilerCommandToActivity(activity, profiledCommand, new StackExchangeRedisInstrumentationOptions(), string.Empty);

Assert.NotNull(result);
Assert.NotNull(result.GetTagValue(SemanticConventions.AttributePeerService));
Expand Down